rguides

purrr::safely

safely() wraps a function so it always returns a list with two components: result and error. If the function succeeds, result has the return value and error is NULL. If it fails, result is NULL (or your otherwise value) and error contains the error object.

Basic Usage

library(purrr)

safe_log <- safely(log)
safe_log(10)
# $result
# [1] 2.302585
# $error
# NULL

safe_log("a")
# $result
# NULL
# $error
# <simpleError in .f(...): non-numeric argument to binary operator>

safely(log) returns a new function that never throws. You always get a result, even when things go wrong.

Signature

safely(.f, otherwise = NULL, quiet = TRUE)
  • .f — the function to wrap
  • otherwise — value to use for result when an error occurs (default NULL)
  • quiet — if TRUE (default), errors are suppressed; if FALSE, errors print as they occur

Inspecting Errors

The error component gives you access to what went wrong:

safe_read <- safely(read.csv)

result <- safe_read("data.csv")

if (is.null(result$result)) {
  message("Failed: ", result$error$message)
  # Handle the failure case
} else {
  df <- result$result
}

This is useful when you need to log errors, retry with different parameters, or decide between multiple fallbacks.

Combining with map and transpose

When mapping over a list where some elements might fail, safely with transpose is a common pattern:

urls <- c(
  "https://api.example.com/data/1",
  "https://api.example.com/data/2",
  "https://api.example.com/data/missing"
)

safe_fetch <- safely(httr::GET)

results <- urls |>
  map(safe_fetch) |>
  transpose()

# Check which failed
errors <- results$error |> keep(Negate(is_null))
successes <- results$result |> keep(Negate(is_null))

transpose() flips the structure so you get a list of all results and a separate list of all errors.

Using otherwise

Set otherwise to a sensible default so you don’t have to check for NULL everywhere:

safe_mean <- safely(mean, otherwise = NA_real_)

x <- list(c(1, 2, 3), "bad", c(4, 5, 6))
x |> map(safe_mean) |> list_c()
# [1]  2 NA  5

otherwise = NA_real_ keeps the output as a numeric vector, which is often what you want when combining with map_dbl or list_c.

safely vs possibly

These are counterparts:

Featuresafely()possibly()
Return on errorlist with result and erroryour chosen default
Typical usewhen you need to inspect the errorwhen you just want a fallback
Output typealways a listcan be simplified
# safely — gives you both result and error
safe_log <- safely(log)
safe_log("a")
# $result: NULL
# $error: <simpleError>

# possibly — gives you a fallback
possibly(log, NA_real_)("a")
# [1] NA

Use safely when you need to know why something failed. Use possibly when you just need a value instead of a crash.

Logging Errors

safe_import <- safely(readr::read_csv)

logs <- vector("list", length(files))

for (i in seq_along(files)) {
  result <- safe_import(files[[i]])
  if (!is.null(result$error)) {
    logs[[i]] <- list(
      file = files[[i]],
      error = result$error$message,
      timestamp = Sys.time()
    )
  }
}

Store errors for later analysis rather than letting them print silently.

See Also