When your package throws an error from an internal function, you have to balance how much of that context you want to show to the user. This involves balancing the error message with the backtrace.
The error message should point to the user’s code - the function that causally led to the problem. The backtrace should show the path down to where the error actually occurred, so that debugging can start from the right place.
I’ll show three approaches to handle this balance, using rlang::abort()
library(rlang)
abort()
# Internal
check1 <- function(x) {
if (!is.character(x)) abort("x must be character")
}
# Exported
my_fun1 <- function(x) check1(x)
my_fun1(123) # Error
last_trace()
<error/rlang_error>
Error in `check1()`:
! x must be character
---
Backtrace:
▆
1. └─global my_fun1(123)
2. └─global check1(x)
Run rlang::last_trace(drop = FALSE) to see 1 hidden frame.
The error focuses on the internal function check1()
. It’s not very actionable for users in the top-level message, but showing it in the backtrace does help identify the internal source of the error.
call = caller_env()
# Internal
check2 <- function(x) {
if (!is.character(x)) abort("x must be character", call = caller_env())
}
# Exported
my_fun2 <- function(x) check2(x)
my_fun2(123) # Error
last_trace()
<error/rlang_error>
Error in `my_fun2()`:
! x must be character
---
Backtrace:
▆
1. └─global my_fun2(123)
Run rlang::last_trace(drop = FALSE) to see 2 hidden frames.
The error message now points to the user-facing function my_fun2()
, which is more actionable. However, only showing the backtrace down to my_fun2()
obscures the internal function that actually threw the error. This is redundant and adds friction for the user, who now needs to last_trace(drop = FALSE)
to see the full path.
call = caller_call()
# Internal
check3 <- function(x) {
if (!is.character(x)) abort("x must be character", call = caller_call())
}
# Exported
my_fun3 <- function(x) check3(x)
my_fun3(123) # Error
last_trace()
<error/rlang_error>
Error in `my_fun3()`:
! x must be character
---
Backtrace:
▆
1. └─global my_fun3(123)
2. └─global check3(x)
Run rlang::last_trace(drop = FALSE) to see 1 hidden frame.
IMO this approach gives you the best of both worlds: the error message points to the user-facing function which is immediately useful, while the backtrace shows the full path down to the internal function which helps further contextualize the error.
The error message and backtrace now serve different purposes instead of being redundant - the error message shows what the user did to cause the error (my_fun3()
), and the backtrace shows where things actually went wrong internally (check3()
).
This post was adapted from a Mastodon exchange: https://fosstodon.org/@yjunechoe/113403050254737010
sessionInfo()
sessionInfo()
#> R version 4.4.1 (2024-06-14 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26100)
#>
#> Matrix products: default
#>
#>
#> locale:
#> [1] LC_COLLATE=English_United States.utf8
#> [2] LC_CTYPE=English_United States.utf8
#> [3] LC_MONETARY=English_United States.utf8
#> [4] LC_NUMERIC=C
#> [5] LC_TIME=English_United States.utf8
#>
#> time zone: America/New_York
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] htmltools_0.5.8.1 rlang_1.1.4
#>
#> loaded via a namespace (and not attached):
#> [1] compiler_4.4.1 fastmap_1.2.0 litedown_0.7.1
#> [4] cli_3.6.3 tools_4.4.1 rstudioapi_0.17.1.9000
#> [7] codetools_0.2-20 digest_0.6.35 xfun_0.52
#> [10] commonmark_1.9.5