rlang 0.3.1

  r-lib, rlang

  Lionel Henry

The patch release 0.3.1 of rlang is now on CRAN! This release polishes the rlang backtraces introduced in 0.3.0. See the NEWS for the complete set of changes. The main feature is rlang::entrace(), that you can set as a global error handler to get rlang backtraces for all errors.

Numbered backtraces

Since rlang 0.3.0, errors thrown with abort() embed a backtrace that can be consulted by calling last_error(). We have improved the appearance of the backtraces by numbering its components. Let’s trigger an error within a complicated call stack:

f <- function() tryCatch(g(), warning = identity) # Try g()
g <- function() evalq(h())                        # Eval h()
h <- function() abort("Oh no!")                   # And fail!

f()
#> Error: Oh no!
#> Call `rlang::last_error()` to see a backtrace

The simplified backtrace shown on error is now numbered:

last_error()
#> <error>
#> message: Oh no!
#> class:   `rlang_error`
#> backtrace:
#>  1. global::f()
#>  6. global::g()
#>  9. global::h()
#> Call `rlang::last_trace()` to see the full backtrace

Notice how the numbering is not sequential? That’s because last_error() displays a simplified backtrace by default, that only includes the calls that are most likely to help you figure out a problem. Call the new function last_trace() to get the full picture:

last_trace()
#>     █
#>  1. └─global::f()
#>  2.   ├─base::tryCatch(g(), warning = identity)
#>  3.   │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
#>  4.   │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>  5.   │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
#>  6.   └─global::g()
#>  7.     ├─base::evalq(h())
#>  8.     │ └─base::evalq(h())
#>  9.     └─global::h()

Note that this full backtrace is the equivalent of base::traceback(), only structured as a tree, presented in reverse order, and with namespace prefixes. Here is the output of traceback() for comparison:

#> 9: h()
#> 8: evalq(h())
#> 7: evalq(h()) at #1
#> 6: g()
#> 5: doTryCatch(return(expr), name, parentenv, handler)
#> 4: tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 3: tryCatchList(expr, classes, parentenv, handlers)
#> 2: tryCatch(g(), warning = identity) at #1
#> 1: f()

Backtraces for base errors!

These backtraces are normally only recorded for errors thrown with abort(). This leaves out errors thrown with stop(), errors thrown from native C code, and warnings converted to errors. Starting from this release, insert this snippet in your RProfile to enable backtraces for all errors:

if (requireNamespace("rlang", quietly = TRUE)) {
  options(error = rlang::entrace)
}

With this in place, any unhandled error will record an rlang backtrace automatically:

h <- function() stop("stop!")
f()
#> Error: stop!
#> Call `rlang::last_error()` to see a backtrace

The backtrace can be consulted in the ordinary way:

last_error()
#> <error>
#> message: stop!
#> class:   `rlang_error`
#> backtrace:
#>  1. global::f()
#>  6. global::g()
#>  9. global::h()
#> Call `rlang::last_trace()` to see the full backtrace
last_trace()
#>     █
#>  1. └─global::f()
#>  2.   ├─base::tryCatch(g(), warning = identity)
#>  3.   │ └─base:::tryCatchList(expr, classes, parentenv, handlers)
#>  4.   │   └─base:::tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>  5.   │     └─base:::doTryCatch(return(expr), name, parentenv, handler)
#>  6.   └─global::g()
#>  7.     ├─base::evalq(h())
#>  8.     │ └─base::evalq(h())
#>  9.     └─global::h()

Both the entracing of base errors and the simplification of backtraces are experimental, but they should already work well enough to be useful in your day-to-day work. We’d love to hear about your feedback!