purrr safely() in R: Catch Errors Without Stopping Code
purrr safely() wraps a function so errors become values instead of crashes. The wrapped function always returns a list with two slots, result and error, so a single bad input never halts a loop.
safely(log) # wrap a function safely(log)(10)$result # get the value safely(log)("a")$error # get the error safely(log, otherwise = NA) # default value on failure map(x, safely(log)) # safe iteration map(x, safely(log)) |> transpose() # split results from errors safely(log, quiet = FALSE) # also print the error
Need explanation? Read on for examples and pitfalls.
What safely() does in one sentence
safely() converts a risky function into a safe one. It takes a function as input and returns a new function. That new function never throws an error: it runs the original code, and whatever happens, hands you back a two-element list. One element holds the successful value, the other holds the error object. Exactly one of them is NULL.
This pattern matters most when you iterate. A normal map() over ten inputs stops dead on the first failure, and you lose the nine results that worked. Wrapping the function with safely() lets every input run to completion so you can inspect failures afterward.
Syntax and arguments
safely() has three arguments. Only the first is required.
.fis the function to wrap. Pass it bare (log), as a string ("log"), or as a lambda (\(x) log(x)).otherwiseis the value placed in theresultslot when the call fails. The default isNULL.quietcontrols whether the error is also printed to the console. The defaultTRUEstays silent; setFALSEwhile debugging.
possibly(), quietly(), and compose(): functions that take a function and return a modified function. The R community calls these "adverbs" because they modify how a verb behaves.Four worked examples
Start with a single safe call. The wrapped function returns a list every time, success or failure.
On success, result holds the value and error is NULL. On failure, the two swap. You test which case you got by checking is.null(out$error).
Supply a fallback with otherwise. Instead of NULL, the result slot can carry a sentinel value you choose.
This keeps result type-stable. Every call now yields a number, so downstream code that expects numeric input never sees a NULL.
Iterate safely over a messy list. This is the core use case. One bad element no longer kills the whole run.
Every input ran. out is a list of four result/error pairs, and only the third one failed.
Split results from errors with transpose(). A list of pairs is awkward to use. transpose() flips it into two parallel lists.
transpose() gives you out$result and out$error as separate lists. compact() drops the NULL entries, leaving the three values that succeeded.
safely() vs possibly() vs quietly()
Pick the adverb that matches what you need back. All three wrap a function, but they return different things.
| Adverb | Returns | Use when |
|---|---|---|
safely() |
list(result, error) |
You need to inspect which inputs failed and why |
possibly() |
the value, or otherwise |
You only want a clean value and can discard failures |
quietly() |
list(result, output, warnings, messages) |
The function prints warnings or messages, not errors |
tryCatch() |
whatever your handler returns | You need custom base-R control flow per error type |
The decision rule is short. Need the error object for logging or a report? Use safely(). Just want the loop to finish and do not care about the failures? possibly() is lighter. Chasing a warning rather than an error? safely() will not help, because warnings are not errors; reach for quietly().
Common pitfalls
safely() returns a function, not a result. A frequent mistake is calling it like safely(log, 100), expecting the log of 100. That passes 100 to the otherwise argument. You must capture the wrapped function first, then call it: safely(log)(100).
The result slot is NULL on failure. Code that pipes out$result straight into arithmetic will break on a NULL. Either set otherwise to a type-stable sentinel, or call compact() to drop the empty slots before you continue.
sqrt(-1) returns NaN with a warning, not an error, so a safely-wrapped sqrt reports success and a NaN result. If you need to trap warnings, wrap with quietly() instead and read its warnings slot.Try it yourself
Try it: Wrap log() with safely() and map it over list(1, "two", 3). Save the captured output to ex_safe and pull out the error from element 2.
Click to reveal solution
Explanation: safely() turns each call into a result/error pair, and transpose() regroups the list of pairs into two parallel lists. That lets you index ex_safe$error to inspect every failure at once.
Related purrr functions
safely() sits in a small family of error-handling adverbs. Reach for a sibling when your need is slightly different.
possibly()returns a plain value or a default, with no error object.quietly()captures printed output, warnings, and messages.map()is the iteration verb safely() is most often paired with.transpose()reshapes the list of result/error pairs.compact()removesNULLentries after a safe run.
For the bigger picture of adverbs and function operators, see the Functional Programming in R guide. The official purrr adverbs reference documents every argument.
FAQ
What is the difference between safely() and possibly() in R?
safely() returns a list with both a result and an error slot, so you can see exactly which inputs failed and read the error message. possibly() returns only the value, or a fallback you specify, and discards the error entirely. Use safely() when you need to report or log failures; use possibly() when you just want the loop to finish and a clean value is enough.
Does safely() catch warnings?
No. safely() only intercepts errors, the conditions that would normally stop execution. Warnings and messages pass through untouched, so a function that emits a warning still counts as a success and its error slot stays NULL. To capture warnings and messages, wrap the function with quietly() instead, which returns dedicated warnings and messages slots.
How do I get just the successful results from a safely() run?
After mapping safely(f) over your inputs, pipe the list into transpose() to split it into $result and $error. Then call compact() on $result to drop the NULL entries left by failed calls. The remaining list holds only the values that succeeded, ready for further processing.
Is safely() part of base R or a package?
safely() comes from the purrr package, part of the tidyverse. Load it with library(purrr). The base-R equivalents are tryCatch() and try(), which give you more manual control but require more boilerplate. safely() is a thin, consistent wrapper designed to compose cleanly with map().