New error style coming up in rlang 1.0.0

This is a companion discussion topic for the original entry at https://www.tidyverse.org/blog/2021/12/rlang-1-0-0-errors


rlang 1.0.0 is getting ready for release and we’d like to get your feedback on the new style of error messages featured in this release.

The rlang package provides several low-level frameworks, like tidy evaluation, for the tidyverse. The 1.0.0 release focuses on one of these frameworks, rlang errors. This set of tools to signal and display errors gets a substantial overhaul. The three main changes to rlang errors that we’ll review in this blog post are:

  1. Fully committing to the display of errors as bulleted lists
  2. Including the erroring function call by default, as in base R
  3. Embracing chained errors to represent contextual information

Attach these packages to follow the examples in the blog post:

library(rlang)
library(dplyr)

Here is how a typical rlang error looked before:

add1 <- function(x) 1 + x

mtcars %>%
group_by(cyl) %>%
mutate(new = add1("foo"))
#> Error: Problem with mutate() column new.
#> :information_source: new = add1("foo").
#> :heavy_multiplication_x: non-numeric argument to binary operator
#> :information_source: The error occurred in group 1: cyl = 4.

And here is how the same error looks with the next versions of rlang and dplyr:

mtcars %>%
  group_by(cyl) %>%
  mutate(new = add1("foo"))
#> Error in `mutate()`:
#> ! Problem while computing `new = add1("foo")`.
#>  The error occurred in group 1: cyl = 4.
#> Caused by error in `1 + x`:
#> ! non-numeric argument to binary operator

For RStudio users, another change is that the error message no longer appears in red but instead uses terminal colours and boldness to style the different parts of the error message.

If you’d like to try the new error style on your computer, install the development versions of rlang and dplyr (the latter needs to be adapted to the new error style) from github with:

pak::pkg_install(c("r-lib/rlang", "tidyverse/dplyr"))

Displaying errors as bullet lists

rlang::abort() makes it easy to structure error messages as a bullet list. We believe that errors should be both informative about the context of the error and easy to skim. A bullet list arrangement that lays out important pieces of information line by line provides the right trade off for this:

abort(c(
  "This is the error message.",
  "*" = "This is a bullet.",
  "*" = "This is another bullet."
))
#> Error:
#> ! This is the error message.
#>  This is a bullet.
#>  This is another bullet.

The bullet symbol can be customised to provide a clue about the kind of information contained in the bullet. Use “ℹ” bullets to provide contextual information or hints, and “✖” bullets to state a problematic input or state.

abort(c(
  "This is the error message.",
  "x" = "Can't do this.",
  "i" = "You could do that instead."
))
#> Error:
#> ! This is the error message.
#>  Can't do this.
#>  You could do that instead.

Here is a dplyr example of an informative error message structured as a bullet list:

mtcars %>%
  group_by(cyl) %>%
  mutate(new = rep(am, 2))
#> Error in `mutate()`:
#> ! Problem while computing `new = rep(am, 2)`.
#>  `new` must be size 11 or 1, not 22.
#>  The error occurred in group 1: cyl = 4.

While rlang has featured error bullets for a while already, the 1.0.0 version fully commits to that format. The main error message (the error header in rlang terms) has become a bullet with a leading “!” sign that makes it easy to skim for error headers in a long R output.

Displaying the erroring function

By default, base::stop() shows the function in which it was called:

add1 <- function(x) {
  if (!is.numeric(x)) {
    stop("`x` must be numeric.")
  }
  x + 1
}

add1("foo")
#> Error in add1("foo"): x must be numeric.

In rlang, we initially decided to turn off that feature because quite often the erroring function is unrelated to the function called by the user. This happens for instance when stop() or abort() are called from a helper function:

add1 <- function(x) {
  check_numeric(x)
  x + 1
}

check_numeric <- function(x) {
if (!is.numeric(x)) {
stop("x must be numeric.")
}
}

add1("foo")
#> Error in check_numeric(x): x must be numeric.

To avoid distracting users with irrelevant information, abort() just didn’t include a call in the error. However, we were missing out on contextual information that could help users understand the origin of an error without having to look at the backtrace, and that context is particularly important in a long pipeline of function calls.

To improve on the situation, we added a call argument to abort() that makes it easy to throw an error on the behalf of another function. If you call abort() from a helper function, pass the caller environment to automatically pick up the corresponding function call:

check_numeric <- function(x) {
  if (!is.numeric(x)) {
    abort("`x` must be numeric.", call = parent.frame())
  }
}

add1("foo")
#> Error in add1():
#> ! x must be numeric.

We have started to adapt our packages to pass the correct function call to abort() but there is still a lot of work to do on that front. If you find a function call that looks off in an error message, please let us know by filing an issue.

Chained errors

Chained errors are another important feature of rlang 1.0. This feature was somewhat hidden in previous versions because it only impacted the appearance of backtraces. In this release, we have decided to show the whole chain of messages to the user, making error chaining much more useful.

One important use case for chaining errors is as a scaffholding for displaying contextual information when the user provides computations nested in a particular step, such as a dplyr verb or a ggplot geom.

mtcars %>%
  group_by(cyl) %>%
  mutate(
    out1 = add1(am),
    out2 = add1("foo")
  )
#> Error in `mutate()`:
#> ! Problem while computing `out2 = add1("foo")`.
#>  The error occurred in group 1: cyl = 4.
#> Caused by error in `add1()`:
#> ! `x` must be numeric.

In this example, dplyr combines all three features (bullet lists, the display of erroring functions, and chained errors) to structure the error message in a hierarchy. At the topmost level, the mutate() error provides information about the current expression being evaluated, as well as the current group. The chained error then displays the function that errored within mutate() as well as the full error message.

Currenty only the development version of dplyr takes advantage of chained errors. We hope to implement them in other tidyverse and tidymodels packages in the coming year to make it easier to detect failing steps in large pipelines.

Use rlang style errors globally

Normally, only the errors thrown with abort() use the new display. Add a call to global_handle() in your .RProfile to use the rlang style globally, including base errors.

# In RProfile
rlang::global_handle()

1 + "foo"
#> Error in `1 + "foo"`:
#> ! non-numeric argument to binary operator

Taking feedback

Given the scope of the changes, we felt it appropriate to delay the release of rlang 1.0 until late January to get more feedback on the new display of errors. Please reach out on twitter (my handle is _lionelhenry) or file an issue on github if you have any comments.