28 Error constructors

28.1 What’s the pattern?

Whenever you generate the same error message (or similar error messages) from different functions you should extract the repeated code into an error constructor. At this point you should also invest in creating a richer error object (aka a custom condition), that makes it easier to test.

(This is a new pattern that is currently used in few places in the tidyverse, but you should expect to see it in more and more packages in the future.)

28.2 What should an error constructor do?

An error constructor is very similar to an S3 constructor, as its job is to extract out repeated code and generate a rich object that can easily be computed with. It should:

  • Be called stop_error_type(). Spend some time on the name as it is part of the exported interface of a function, so changing it later will require work.

  • Have one argument for each varying part of the error. Each argument is used in two ways: in a call to glue::glue() that generates an error message designed to be helpful to a human, and passed on to rlang::abort() so it is available for testing.

  • Create and throw an error using rlang::abort() with a custom subclass of the form pkgname_error_type (i.e. it should add pkgname_ prefix to the name of the function after removing stop_).

  • If the error is subclassable, it should also have ... and class arguments, in the same way an S3 constructor does.

28.3 Why is this important?

  • Makes testing more robust by decoupling tests of the error from the error message.

  • Allows more precise control over error handling with tryCatch()

  • Improves documentation by clearing describing the failure modes of a function.

28.4 What are some examples?

stop_request_failed <- function(code, message, status) {
  message <- glue::glue(
    "HTTP error {code}\n",
    "  * message: {sq(message)}\n",
    "  * status: {sq(status)}"
  # needed because httr doesn't export stop_request()
  class <- c("gargle_error_request_failed", paste0("http_error_", code))
    .subclass = class,
    message = message,
    status = status

28.5 How do I document?

Generally these error constructors should be exported so that you can document them. Then in the documentation of the function that calls them, you should include a @section errors that briefly describes when each error is generated, with a link to its documentation.=

28.6 How do I test?

28.6.1 Testing the constructor.

Firstly, you should test the constructor itself. The primary goal is to ensure that the error constructor generates a message that is useful to humans, which you can not automate. This means that you need to use a regression test, so you can ensure that the message does not change unexpectedly.

Until testthat has expect_known_output() best option is to catch_cnd() and use expect_known_output().

cnd <- catch_cnd(stop_my_function())
expect_known_output(cnd, "test-stop-my-function-message.txt", print = TRUE)

28.6.2 Testing usage

Once you have an error constructor, you can now to switch from testing the text of the error message (i.e. expect_error(foofy(), "a text fragment")) to checking for the specific class generated (i.e. expect_error(foofy(), class = "pkgname_error_goofy")). This makes the test less fragile as it will no longer fail if you change the error message. It is particularly important to test for the class of the error message when the error is generated by a function in another package; otherwise it is very easy to introduce a tight coupling to the specific version of the depenedency.

Occassionally, you will want to be more specific, and assert that specific components of the error are as you expect. In this case, capture the return value of expect_error() and then test the components:

err <- expect_error(foofy(1, 2), class = "pkgname_error_bad_input")
expect_equal(err$sum, 3)

I don’t think this level of testing is generally important, so you should only use it because the error generation code is complex conditions, or you have identified a bug that you want to assuredly fix.