purrr::safely() / purrr::possibly() / purrr::quietly()
safely(.f, otherwise = NULL, quiet = TRUE)
possibly(.f, otherwise = NULL, quiet = TRUE)
quietly(.f) The safely(), possibly(), and quietly() functions from purrr wrap existing functions to handle errors, warnings, and messages gracefully. These wrappers are essential when applying functions across many elements—such as rows of a data frame or elements of a list—where a single failure would abort the entire operation.
Syntax
library(purrr)
# safely: captures both errors and results
safely(.f, otherwise = NULL, quiet = TRUE)
# possibly: returns a default value on error
possibly(.f, otherwise = NULL, quiet = TRUE)
# quietly: captures warnings and messages but lets errors fail
quietly(.f)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
.f | function | , | The function to wrap |
otherwise | any | NULL | Value to return on error (possibly only) |
quiet | logical | TRUE | Suppress warnings and messages |
Examples
Basic usage with safely()
safely() returns a list with two elements: result and error. If the function succeeds, result contains the output and error is NULL. If it fails, result is NULL and error contains the error condition.
library(purrr)
# Wrap a function that sometimes fails
safe_divide <- safely(function(x, y) {
if (y == 0) stop("Division by zero")
x / y
})
# Successful call
safe_divide(10, 2)
# $result
# [1] 5
#
# $error
# NULL
# Failed call
safe_divide(10, 0)
# $result
# NULL
#
# $error
# <simpleError: Division by zero>
The structured output of safely() — always returning a two-element list — means your downstream code never has to guess whether a call succeeded. You can check is.null(result$error) to branch on success versus failure. When you do not need the error detail and simply want a fallback value, possibly() offers a lighter syntax that returns your chosen default directly without the wrapper list.
Using possibly() for default values
possibly() is a convenience wrapper that returns a specified default value instead of an error.
library(purrr)
# Return NA on failure
safe_log <- possibly(log, otherwise = NA_real_)
# Works fine
safe_log(10)
# [1] 2.302585
# Returns NA instead of error
safe_log(-10)
# [1] NA
The trade-off between safely() and possibly() is about how much error detail you need downstream. When you are building a batch processing pipeline where some inputs are expected to fail, wrapping the core operation with safely() and then filtering successes from failures with map_lgl() and is.null() gives you a clean way to separate the good results from the problematic ones without losing the error information.
Combining with map()
These wrappers shine when used with map() to process multiple items where some might fail.
library(purrr)
# Vector of values to process
values <- c(100, 25, -5, 16, 0)
# Apply safely across all values
results <- map(values, safely(function(x) {
if (x <= 0) stop("Must be positive")
log(x)
}))
# Check which succeeded
succeeded <- map_lgl(results, ~is.null(.x$error))
succeeded
# [1] TRUE TRUE FALSE TRUE FALSE
# Extract successful results
map_dbl(results[succeeded], "result")
# [1] 4.605170 3.218876 2.772589
Filtering successes with map_lgl(results, ~is.null(.x$error)) is the standard post-processing idiom after a batch safely() call. A related wrapper, quietly(), addresses a different concern: it captures warnings and messages produced by a function without suppressing them or halting execution. This is useful when you expect certain operations to generate warnings — like taking the square root of a negative number — and you want to log them for later review rather than have them scroll past in the console.
Using quietly() to capture output
quietly() captures warnings and messages without suppressing errors.
library(purrr)
quiet_sqrt <- quietly(sqrt)
# Normal call with warning
quiet_sqrt(-4)
# $result
# [1] NaN
#
# $warnings
# [1] "NaNs produced"
#
# $messages
# character(0)
#
# $result
# [1] NaN
The next example demonstrates error-tolerant data import, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.
Common patterns
Error-tolerant data import
library(purrr)
library(readr)
# Try multiple file paths, continue on failure
file_paths <- c("data1.csv", "data2.csv", "data3.csv")
safe_read <- possibly(read_csv, otherwise = NULL)
data_list <- map(file_paths, safe_read)
valid_data <- keep(data_list, ~!is.null(.x))
The next example demonstrates conditional processing pipelines, building on the pattern established above and showing how the function behaves with a different set of inputs or arguments. Working through these variations step by step reinforces how each parameter affects the output and builds the muscle memory you need to reach for the right function in your own R scripts without having to consult the documentation every time.
Conditional processing pipelines
library(purrr)
library(dplyr)
# Process each row with potential failures
results <- df %>%
mutate(safe_result = map2(col_a, col_b, ~possibly(
function(x, y) compute_something(x, y),
otherwise = NA
)(.x, .y)))
purrr::safely() in practice
safely() wraps a function so it always returns a list with two elements: $result (the function’s return value, or NULL on error) and $error (the error object, or NULL on success). Exactly one of the two will be non-NULL. This structure lets you process a list of inputs and collect all results and errors without the whole operation failing on the first error.
The standard pattern after map(inputs, safely(f)) is to split results from errors: results <- map(out, "result") extracts all results (some NULL), and errors <- map(out, "error") extracts all errors. keep(results, ~!is.null(.)) or compact(results) drops the NULL failures. keep(errors, ~!is.null(.)) collects the failures for inspection or logging.
safely() accepts an otherwise argument that replaces NULL in the $result slot on error, making it easier to bind results into a data frame: map_dfr(inputs, safely(f, otherwise = tibble())) returns a data frame even when some inputs fail, with the otherwise tibble used as a placeholder for failures.
Compared to possibly(), safely() is more appropriate when you need to diagnose or log failures rather than silently skip them. In production pipelines, safely() is preferred: it surfaces errors for monitoring, while possibly() swallows them. Reserve possibly() for cases where failures are expected, benign, and not worth logging.
safely() wraps a function so it returns a list with result and error elements instead of throwing. The result is NULL on failure; error is NULL on success. After mapping with safely(), extract results with map(results, "result") and errors with map(results, "error"). This pattern is useful for processing many files or API calls where some are expected to fail.