R purrr - possibly() and safely() - Error Handling
• `possibly()` and `safely()` transform functions into error-resistant versions that return default values or captured error objects instead of halting execution
Key Insights
• possibly() and safely() transform functions into error-resistant versions that return default values or captured error objects instead of halting execution
• safely() returns a list with result and error elements, enabling detailed error inspection and conditional logic based on failure types
• These functional programming tools excel in data pipelines where partial failures shouldn’t stop processing, particularly when mapping over lists or API calls
Why Standard Error Handling Falls Short in Functional Pipelines
When applying functions across lists using map(), a single error stops the entire operation. Traditional tryCatch() requires verbose boilerplate that clutters code and breaks the functional flow.
library(purrr)
library(dplyr)
# Standard approach fails on first error
urls <- c("https://api.example.com/data/1",
"https://api.example.com/data/2",
"invalid_url",
"https://api.example.com/data/4")
# This would stop at the invalid URL
# results <- map(urls, readLines)
# Error in file(con, "r") : cannot open the connection
The purrr package provides possibly() and safely() as functional wrappers that handle errors gracefully without breaking pipelines. They transform any function into a failure-tolerant version.
Using possibly() for Simple Default Values
possibly() wraps a function and returns a default value when errors occur. The syntax is possibly(function, otherwise = default_value).
# Create error-resistant version of a function
safe_log <- possibly(log, otherwise = NA_real_)
# Works with valid input
safe_log(10)
# [1] 2.302585
# Returns default instead of error
safe_log("invalid")
# [1] NA
# Perfect for mapping operations
values <- list(100, "text", -5, 50, NULL, 25)
results <- map_dbl(values, safe_log)
results
# [1] 4.605170 NA NaN 3.912023 NA 3.218876
The otherwise parameter accepts any value. Choose defaults that make sense for your data type:
# Different default values for different contexts
safe_parse_number <- possibly(as.numeric, otherwise = 0)
safe_read_file <- possibly(readLines, otherwise = character(0))
safe_api_call <- possibly(httr::GET, otherwise = NULL)
# Using in a data pipeline
data <- tibble(
id = 1:5,
value = c("123", "456", "invalid", "789", "abc")
)
data %>%
mutate(numeric_value = map_dbl(value, safe_parse_number))
# # A tibble: 5 × 3
# id value numeric_value
# <int> <chr> <dbl>
# 1 1 123 123
# 2 2 456 456
# 3 3 invalid 0
# 4 4 789 789
# 5 5 abc 0
Using safely() for Detailed Error Inspection
safely() provides more control by returning a list with two elements: result (the successful output or NULL) and error (NULL or the error object).
safe_sqrt <- safely(sqrt)
# Successful execution
safe_sqrt(16)
# $result
# [1] 4
#
# $error
# NULL
# Failed execution
safe_sqrt("invalid")
# $result
# NULL
#
# $error
# <simpleError in sqrt("invalid"): non-numeric argument to mathematical function>
This structure enables sophisticated error handling logic:
# Process files with detailed error tracking
files <- c("data1.csv", "missing.csv", "data2.csv")
safe_read <- safely(read.csv)
results <- map(files, safe_read)
# Extract successful results
successful <- results %>%
keep(~ is.null(.x$error)) %>%
map("result")
# Extract and analyze errors
errors <- results %>%
keep(~ !is.null(.x$error)) %>%
map_chr(~ .x$error$message)
errors
# [1] "cannot open file 'missing.csv': No such file or directory"
Practical Pattern: API Calls with Error Logging
Combining safely() with transpose() creates clean error-handling workflows for API operations:
library(httr)
library(jsonlite)
# Simulate API call function
fetch_user <- function(id) {
if (id > 100) stop("User ID out of range")
list(id = id, name = paste0("User", id), status = "active")
}
safe_fetch <- safely(fetch_user)
user_ids <- c(1, 50, 150, 75, 200)
results <- map(user_ids, safe_fetch)
# Transpose to separate results and errors
transposed <- transpose(results)
# Get all successful results
valid_users <- transposed$result %>%
compact() # Remove NULLs
# Get all errors with their indices
error_indices <- which(!map_lgl(transposed$error, is.null))
error_messages <- transposed$error %>%
compact() %>%
map_chr("message")
# Create error report
error_report <- tibble(
user_id = user_ids[error_indices],
error = error_messages
)
error_report
# # A tibble: 2 × 2
# user_id error
# <dbl> <chr>
# 1 150 User ID out of range
# 2 200 User ID out of range
Advanced Pattern: Retry Logic with safely()
Build retry mechanisms by checking error types:
# Function that fails randomly
unreliable_api <- function(x) {
if (runif(1) < 0.7) stop("Connection timeout")
x * 2
}
# Retry wrapper using safely()
retry_call <- function(f, x, max_attempts = 3) {
safe_f <- safely(f)
for (attempt in seq_len(max_attempts)) {
result <- safe_f(x)
if (is.null(result$error)) {
return(result$result)
}
if (attempt < max_attempts) {
message(sprintf("Attempt %d failed: %s. Retrying...",
attempt, result$error$message))
Sys.sleep(0.5)
}
}
stop(sprintf("Failed after %d attempts: %s",
max_attempts, result$error$message))
}
# Use with map
set.seed(42)
values <- 1:5
results <- map_dbl(values, ~ retry_call(unreliable_api, .x))
Combining possibly() and safely() with quietly()
The purrr package also provides quietly() for capturing warnings and messages:
noisy_function <- function(x) {
if (x < 0) warning("Negative value detected")
if (x == 0) message("Processing zero")
sqrt(abs(x))
}
quiet_sqrt <- quietly(noisy_function)
result <- quiet_sqrt(-4)
result
# $result
# [1] 2
#
# $output
# [1] ""
#
# $warnings
# [1] "Negative value detected"
#
# $messages
# character(0)
# Combine with safely() for comprehensive error handling
safe_quiet <- safely(quietly(noisy_function))
Performance Considerations
Error-handling wrappers add minimal overhead but accumulate in large-scale operations:
library(bench)
test_data <- 1:10000
mark(
standard = map_dbl(test_data, sqrt),
possibly = map_dbl(test_data, possibly(sqrt, otherwise = NA)),
safely = map(test_data, safely(sqrt)),
check = FALSE
)
# # A tibble: 3 × 6
# expression min median `itr/sec` mem_alloc `gc/sec`
# <bch:expr> <bch:tm> <bch:tm> <dbl> <bch:byt> <dbl>
# 1 standard 1.2ms 1.35ms 710. 78.2KB 2.08
# 2 possibly 1.89ms 2.05ms 476. 156.3KB 2.11
# 3 safely 8.45ms 9.12ms 108. 1.79MB 6.38
Use possibly() when you only need default values. Reserve safely() for situations requiring error inspection or conditional logic based on failure types.
Choosing Between possibly() and safely()
Use possibly() when:
- You need simple default values for failures
- Performance is critical with large datasets
- Errors don’t require investigation
- Working with
map_dbl(),map_chr(), or other typed variants
Use safely() when:
- You need to log or analyze errors
- Different errors require different handling
- Building retry mechanisms
- Debugging pipelines with intermittent failures
Both functions transform error-prone operations into robust, production-ready code without sacrificing the elegance of functional programming patterns.