7 Conditions

7.1 Introduction

The condition system provides a paired set of tools that allow the author of a function to indicate that something unusual is happening, and the user of that function to deal with it. The function author signals conditions with functions like stop() (for errors), warning() (for warnings), and message() (for messages), then the function user can handle them with functions like tryCatch() and withCallingHandlers(). Understanding the condition system is important because you’ll often need to play both roles: signalling conditions from the functions you create, and handle conditions signalled by the functions you call.

R offers a very powerful condition system based on ideas from Common Lisp. Like R’s approach to object oriented programming, it is rather different to currently popular programming languages so it is easy to misunderstand, and there has been relatively little written about how to use it effectively. Historically, this has lead to few people (including me!) taking full advantage of its power. The goal of this chapter is to remedy that situation. Here you will learn about the big ideas of R’s condition system, as well as learning a bunch of practical tools that will make your code stronger.

I found two resources particularly useful when writing this chapter. You may also want to read them if you want to learn more about the inspirations and motivations for the system:

I also found it helpful to work through the underlying C code that implements these ideas. If you’re interested in understanding how it all works, you might find my notes to be useful.

Quiz

Want to skip this chapter? Go for it, if you can answer the questions below. Find the answers at the end of the chapter in answers.

  1. What are the three most important types of condition?

  2. What function do you use to ignore errors in block of code?

  3. What’s the main difference between tryCatch() and withCallingHandlers()?

  4. Why might you want to create a custom error object?

Overview

  • Signalling conditions introduces the basic tools for signalling conditions, and discusses when it is appropriate to use each type.

  • Ignoring conditions teaches you about the simplest tools for handling conditions: functions like try() and supressMessages() that swallow conditions and prevent them from getting to the top level.

  • Handling conditions introduces the condition object, and the two fundamental tools of condition handling: tryCatch() for error conditions, and withCallingHandlers() for everything else.

  • Custom conditions shows you how to extend the built-in condition objects to store useful data that condition handlers can use to make more informed decisions.

  • Applications closes out the chapter with a grab bag of practical applications based on the low-level tools found in earlier sections.

7.1.1 Prerequisites

As well as base R functions, this chapter uses condition signalling and handling functions from rlang.

7.2 Signalling conditions

There are three conditions that you can signal in code: errors, warnings, and messages.

  • Errors are the most severe; they indicate that there is no way for a function to continue and execution must stop.

  • Messages are the mildest; they are way of informing the user that some action has been performed on their behalf.

  • Warnings fall somewhat in between, and typically indicate that something has gone wrong but the function has been able to at least partially recover.

There is a final condition that can only be generated interactively: an interrupt, which indicates that the user has “interrupted” execution by pressing Escape, Ctrl + Break, or Ctrl + C (depending on the platform).

Conditions are usually displayed prominently, in a bold font or coloured red, depending on the R interface. You can tell them apart because errors always start with “Error”, warnings with “Warning message”, and messages with nothing.

The following three sections describe errors, warnings, and message in more detail.

7.2.1 Errors

In base R, errors are signalled, or thrown, by stop():

By default, the error message includes the call, but this is typically not useful (and recapitulates information that you can easily get from traceback()), so I think it’s good practice to use call. = FALSE:

The rlang equivalent to stop(), rlang::abort(), does this automatically. We’ll use abort() throughout this chapter, but we won’t get to its most compelling feature, the ability to add additional metadata to the condition object, until we’re near the end of the chapter.

(Note that stop() pastes together multiple inputs, while abort() does not. To create complex error messages with abort, I recommend using glue::glue(). This allows us to use other arguments to abort() for useful features that you’ll learn about in custom conditions.)

The best error messages tell you what is wrong and point you in the right direction to fix the problem. Writing good error messages is hard because errors usually occur when the user has a flawed mental model of the function. As a developer, it’s hard to imagine how the user might be thinking incorrectly about your function, and thus it’s hard to write a message that will steer the user in the correct direction. That said, the tidyverse style guide discusses a few general principles that we have found useful: http://style.tidyverse.org/error-messages.html.

7.2.2 Warnings

Warnings, signalled by warning(), are weaker than errors: they signal that something has gone wrong, but the code has been able to recover and continue. Unlike errors, you can have multiple warnings from a single function call:

By default, warnings are cached and printed only when control returns to the top level:

You can control this behaviour with the warn option:

  • To make warnings appear immediately, set options(warn = 1).

  • To turn warnings into errors, set options(warn = 2). This is usually the easiest way to debug a warning, as once it’s an error you can use tools like traceback() to find the source.

  • Restore the default behaviour with option(warn = 0).

Like stop(), warning() also has a call argument. It is slightly more useful (since warnings are often more distant from their source), but I still generally suppress it with call. = FALSE. Like rlang::abort(), the rlang equivalent of warning(), rlang::warn(), also suppresses the call. by default.

Warnings occupy a somewhat challenging place between messages (“you should know about this”) and errors (“you must fix this!”), and it’s hard to give precise advice on when to use them. Generally, be restrained, as warnings are easy to miss if there’s a lot of other output, and you don’t want your function to recover too easily from clearly invalid input. In my opinion, base R tends to overuse warnings, and many warnings in base R would be better off as errors. For example, I think these warnings would be more helpful as errors:

There only a couple of cases where using a warning is clearly appropriate:

  • When you deprecate a function you want to allow older code to continue to work (so ignoring the warning is ok) but you want to encourage the user to switch to a new function.

  • When you are reasonably certain you can recover from a problem: If you were 100% certain that you could fix the problem, you wouldn’t need any message; if you were more uncertain that you could correctly fix the issue, you’d throw an error.

Otherwise use warnings with restraint, and carefully consider if an error would be more appropriate.

7.2.3 Messages

Messages, signalled by message(), are informational; use them to tell the user that you’ve done something on their behalf. Good messages are a balancing act: you want to provide just enough information so the user knows what’s going on, but not so much that they’re overwhelmed.

messages() are displayed immediately and do not have a call. argument:

Good places to use a message are:

  • When a default argument requries some non-trivial amount of computation and you want to tell the user what value was used. For example, ggplot2 reports the number of bins used if you don’t supply a binwidth.

  • In functions that are called primarily for their side-effects which would otherwise be silent. For example, when writing files to disk, calling a web API, or writing to a database, it’s useful provide regular status messages telling the user what’s happening.

  • When you’re about to start a long running process with no intermediate output. A progress bar (e.g. with progress) is better, but a message is a good place start.

  • When writing a package, you sometimes want to display a message when your package is loaded (i.e. in .onAttach()); here you must use packageStartupMessage().

Generally any function that produces a message should have some way to suppress it, like a quiet = TRUE argument. It is possible to suppress all messages with suppressMessages(), as you’ll learn shortly, but it is nice to also give finer grained control.

It’s important to compare message() to the closely related cat(). In terms of usage and result, they appear quite similar31:

However, the purposes of cat() and message() are different. Use cat() when the primary role of the function is to print to the console, like print() or str() methods. Use message() as a side-channel to print to the console when the primary purpose of the function is something else. In other words, cat() is for when the user asks for something to be printed and message() is for when the developer elects to print something.

7.2.4 Exercises

  1. Write a wrapper around file.remove() that throws an error if the file to be deleted does not exist.

  2. What does the appendLF argument to message() do? How is it related to cat()?

  3. What does options(error = recover) do? Why might you use it?

  4. What does options(error = quote(dump.frames(to.file = TRUE))) do? Why might you use it?

7.3 Ignoring conditions

The simplest way of handling conditions in R is to simply ignore them:

  • Ignore errors with try().
  • Ignore warnings with suppressWarnings().
  • Ignore messages with suppressMessages().

These functions are heavy handed as you can’t use them to suppress a single type of condition that you know about, while allowing everything else to pass through. We’ll come back to that challenge later in the chapter.

try() allows execution to continue even after an error has occurred. Normally if you run a function that throws an error, it terminates immediately and doesn’t return a value:

However, if you wrap the statement that creates the error in try(), the error message will be displayed32 but execution will continue:

It is possible, but not recommended, to save the result of try() and perform different actions based on whether or not the code succeed or failed33. Instead, it is better to use tryCatch() or a higher-level helper; you’ll learn about those shortly. A simple, but useful, pattern is to do assignment inside the call: this lets you define a default value to be used if the code does not succeed.

suppressWarnings() and suppressMessages() suppress all warnings and messages. Unlike errors, messages and warnings don’t terminate execution, so there may be multiple signalled in a single block.

7.4 Handling conditions

Every condition has default behaviour: errors stop execution and return to the top level, warnings are captured and displayed in aggregate, and messages are immediately displayed. Condition handlers allow us to temporarily override or supplement the default behaviour.

Two functions, tryCatch() and withCallingHandlers(), allow us to register handlers, functions that take the signalled condition as their single argument. The registration functions have the same basic form:

They differ in the type of handlers that they create:

  • tryCatch() defines exiting handlers; after the condition is handled, control returns to the context where tryCatch() was called. This makes tryCatch() most suitable for working with errors and interrupts, as these have to exit anyway.

  • withCallingHandlers() defines calling handlers; after the condition is captured control returns to the context where the condition was signalled. This makes it most suitable for working with non-error conditions.

But before we can learn about and use these handlers, we need to talk a little bit about condition objects. These are created implicitly whenever you signal a condition, but become explicit inside the handler.

7.4.1 Condition objects

So far we’ve just signalled conditions, and not looked at the objects that are created behind the scenes. The easiest way to see a condition object is to catch one from a signalled condition. That’s the job of rlang::catch_cnd():

cnd <- catch_cnd(abort("An error"))
str(cnd)
#> List of 4
#>  $ message: chr "An error"
#>  $ call   : NULL
#>  $ trace  :List of 3
#>   ..$ calls  :List of 31
#>   .. ..$ : language local({     args = commandArgs(TRUE) ...
#>   .. ..$ : language eval.parent(substitute(eval(quote(expr), envir)))
#>   .. ..$ : language eval(expr, p)
#>   .. ..$ : language eval(expr, p)
#>   .. ..$ : language eval(quote({     args = commandArgs(TRUE) ...
#>   .. ..$ : language eval(quote({     args = commandArgs(TRUE) ...
#>   .. ..$ : language do.call(rmarkdown::render, c(args[1], readRDS(args[2"..
#>   .. ..$ : language (function (input, output_format = NULL, output_file =..
#>   .. ..$ : language knitr::knit(knit_input, knit_output, envir = envir, q..
#>   .. ..$ : language process_file(text, output)
#>   .. ..$ : language withCallingHandlers(if (tangle) process_tangle(group)..
#>   .. ..$ : language process_group(group)
#>   .. ..$ : language process_group.block(group)
#>   .. ..$ : language call_block(x)
#>   .. ..$ : language block_exec(params)
#>   .. ..$ : language in_dir(input_dir(), evaluate(code, envir = env, new_d..
#>   .. ..$ : language evaluate(code, envir = env, new_device = FALSE, keep_..
#>   .. ..$ : language evaluate::evaluate(...)
#>   .. ..$ : language evaluate_call(expr, parsed$src[[i]], envir = envir, e..
#>   .. ..$ : language timing_fn(handle(ev <- withCallingHandlers(withVisibl..
#>   .. ..$ : language handle(ev <- withCallingHandlers(withVisible(eval(exp..
#>   .. ..$ : language withCallingHandlers(withVisible(eval(expr, envir, enc..
#>   .. ..$ : language withVisible(eval(expr, envir, enclos))
#>   .. ..$ : language eval(expr, envir, enclos)
#>   .. ..$ : language eval(expr, envir, enclos)
#>   .. ..$ : language catch_cnd(abort("An error"))
#>   .. ..$ : language tryCatch(condition = identity, {     force(expr) ...
#>   .. ..$ : language tryCatchList(expr, classes, parentenv, handlers)
#>   .. ..$ : language tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>   .. ..$ : language doTryCatch(return(expr), name, parentenv, handler)
#>   .. ..$ : language force(expr)
#>   ..$ parents: int [1:31] 0 1 2 3 0 5 6 6 8 9 ...
#>   ..$ envs   :List of 31
#>   .. ..$ : chr "0x3a805a0"
#>   .. ..$ : chr "0x3a80450"
#>   .. ..$ : chr "0x3a80108"
#>   .. ..$ : chr "global"
#>   .. ..$ : chr "0x3a7fa78"
#>   .. ..$ : chr "0x3a7f5e0"
#>   .. ..$ : chr "0x3a817d8"
#>   .. ..$ : chr "0x3bed628"
#>   .. ..$ : chr "0x27c9b40"
#>   .. ..$ : chr "0x21a31b0"
#>   .. ..$ : chr "0x779ce60"
#>   .. ..$ : chr "0x77a26a8"
#>   .. ..$ : chr "0x77a2408"
#>   .. ..$ : chr "0x77a2328"
#>   .. ..$ : chr "0x77835c8"
#>   .. ..$ : chr "0x727e168"
#>   .. ..$ : chr "0x727cca0"
#>   .. ..$ : chr "0x727f2c0"
#>   .. ..$ : chr "0x7311ac0"
#>   .. ..$ : chr "0x7331e18"
#>   .. ..$ : chr "0x7331d38"
#>   .. ..$ : chr "0x7331a28"
#>   .. ..$ : chr "0x7331478"
#>   .. ..$ : chr "0x7331248"
#>   .. ..$ : chr "global"
#>   .. ..$ : chr "0x7334588"
#>   .. ..$ : chr "0x7334390"
#>   .. ..$ : chr "0x7333d00"
#>   .. ..$ : chr "0x73339b8"
#>   .. ..$ : chr "0x7333670"
#>   .. ..$ : chr "0x7333328"
#>   ..- attr(*, "class")= chr "rlang_trace"
#>  $ parent : NULL
#>  - attr(*, "class")= chr [1:3] "rlang_error" "error" "condition"

Built-in conditions are lists with two elements:

  • message, a length-1 character vector containing the text to display to a user. To extract the message, use conditionMessage(cnd).

  • call, the call which triggered the condition. As described above, we don’t use the call, so it will often be NULL. To extract it, use conditionCall(cnd).

Custom conditions may contain other components, which we’ll discuss in custom conditions.

Conditions also have a class attribute, which makes them S3 objects. We won’t disucss S3 until S3, but fortunately, even if you don’t know about S3, condition objects are quite simple. The most important thing to know is that the class attribute is a character vector, and it determines which handlers will match the condition.

7.4.2 Exiting handlers

tryCatch() registers exiting handlers, and is typically used to handle error conditions. It allows you to override the default error behaviour. For example, the following code will return 10 instead of display an error:

If no conditions are signalled, or the class of the signalled condition does not match the handler name, the code executes normally:

The handlers set up by tryCatch() are called exiting handlers because after the condition is signalled, control passes to the handler and never returns to the original code, effectively meaning that the code “exits”:

Note that the code is evaluated in the environment of tryCatch(), but the handler code is not, because the handlers are functions. This is important to remember if you’re trying to modify objects in the parent environment.

The handler functions are called with a single argument, the condition object. I call this argument cnd, by convention. This value is only moderately useful for the base conditions because they contain relatively little data. It’s more useful when you make your own custom conditions, as you’ll see shortly.

tryCatch() has one other argument: finally. It specifies a block of code (not a function) to run regardless of whether the initial expression succeeds or fails. This can be useful for clean up, like deleting files, or closing connections. This is functionally equivalent to using on.exit() (and indeed that’s how it’s implemented) but it can wrap smaller chunks of code than an entire function.

7.4.3 Calling handlers

The handlers set up by tryCatch() are called exiting handlers, because they cause code to exit once the condition has been caught. By contrast, withCallingHandler() sets up calling handlers: code execution continues normally once the handler returns. This tends to make withCallingHandlers() a more natural pairing with the non-error conditions.

Compare the results of tryCatch() and withCallingHandlers() in the example below. The message are not printed in the first case, because the code is terminated once the exiting handler completes. They are printed in the second case, because a calling handler does not exit.

Handlers are applied in order, so you don’t need to worry getting caught in an infinite loop:

(But beware if you have multiple handlers, and some handlers signal conditions that could be captured by another handler: you’ll need to think through the order carefully.)

The return value of a calling handler is ignored because the code continues to execute after the handler completes; where would the return value go? That means that calling handlers are only useful for their side-effects.

One important side-effect unique to calling handlers is the ability to muffle the signal. By default, a condition will continue to propogate to parent handlers, all the way up to the default handler (or an exiting handler, if provided):

If you want to prevent the condition “bubbling up” but still run the rest of the code in the block, you need to explicitly muffle it with rlang::cnd_muffle():

7.4.4 Call stacks

To complete the section, there are some important differences between the call stacks of exiting and calling handlers. These differences are generally not important but I’m including it here because I’ve occassionally found it useful, and don’t want to forget about it!

It’s easiest to see the difference by setting up a small example that uses lobstr::cst():

Calling handlers are called in the context of the call that signalled the condition:

Whereas exiting handlers are called in the context of the call to tryCatch():

7.5 Custom conditions

One of the challenges of error handling in R is that most functions generate one of the built-in conditions, which contain only a message and a call. That means that if you want to detect a specific type of error, you can only work with the text of the error message. This is error prone, not only because the message might change over time, but also because messages can be translated into other languages.

Fortunately R has a powerful, but little used feature: the ability to create custom conditions that can contain additional metadata. Creating custom conditions is a little fiddly in base R, but rlang::abort() makes it very easy as you can supply a custom .subclass and additional metadata.

The following example shows the basic pattern. I recommend using the following call structure for custom conditions. This takes advantage of R’s flexible argument matching so that the name of the “type” of error comes first, followed by the user facing text, followed by custom metadata.

Custom conditions work just like regular conditions when used interactively, but allow handlers to do much more.

7.5.1 Motivation

To explore these ideas in more depth, let’s take base::log(). It does the minimum when throwing errors caused by invalid arguments:

I think we can do better by being explicit about which argument is the problem (i.e. x or base), and saying what the problematic input is (not just what it isn’t).

This gives us:

This is an improvement for interactive usage as the error messages are more likely to guide the user towards a correct fix. However, they’re no better if you want to programmatically handle the errors: all the useful metadata about the error is jammed into a single string.

7.5.2 Signalling

Let’s build some infrastructure to improve this situtation, We’ll start by providing a custom abort() function for bad arguments. This is a little over-generalised for the example at hand, but it reflects common patterns that I’ve seen across other functions. The pattern is fairly simple. We create a nice error message for the user, using glue::glue(), and store metadata in the condition call for the developer.

We can now rewrite my_log() to use this new helper:

my_log() itself is not much shorter, but is a little more meanginful, and it ensures that error messages for bad arguments are consistent across functions. It yields the same interactive error messages as before:

7.5.3 Handling

These structured condition objects are much easier to program with. The first place you might want to use this capability is when testing your function. Unit testing is not a subject of this book (see R packages for details), but the basics are easy to understand. The following code captures the error, and then asserts it has the structure that we expect.

We can also use the class (error_bad_argument) in tryCatch() to only handle that specfic error:

Note that when using tryCatch() with multiple handlers and custom classes, the first handler to match any class in the signal’s class vector is called, not the best match. For this reason, you need to make sure to put the most specific handlers first. The following code does not do what you might hope:

7.5.4 Exercises

  1. Inside a package, it’s occassionally useful to check that a package is installed before using it. Write a function that checks if a package is installed (with requireNamespace("pkg", quietly = FALSE)) and if not, throws a custom condition that includes the package name in the metadata.

  2. Inside a package you often need to stop with an error when something is not right. Other packages that depend on your package might be tempted to check these errors in their unit tests. How could you help these packages to avoid relying on the error message which is part of the user interface rather than the API and might change without notice?

7.6 Applications

Now that you’ve learned the basic tools of R’s condition system, it’s time to dive into some applications. The goal of this section is not to show every possible usage of tryCatch() and withCallingHandlers() but to illustrate some common patterns that frequently crop up. Hopefully these will get your creative juices flowing, so when you encounter a new problem you can come up with a useful solution.

7.6.1 Failure value

There are a few simple, but useful, tryCatch() patterns based on returning a value from the error handler. The simplest case is a wrapper to return a “default” value if an error occurs:

A more sophisticated application is base::try(). Below, try2() extracts the essence of base::try(); the real function is more complicated in order to make the error message look more like what you’d see if tryCatch() wasn’t used.

7.6.2 Success and failure values

We can extend this pattern to returns one value if the code evaluates successfully (success_val), and another if it fails (error_val). This pattern just requires one small trick: evaluating the user supplied code, then success_val. If the code throws an error, we’ll never get to success_val and will instead return error_val.

We can use this to determine if an expression fails:

Or to capture any condition, like just rlang::catch_cnd():

We can also use this pattern to create a try() variant. One challenge with try() is that it’s slightly challenging to determine if the code succeeded or failed. Rather than returning an object with a special class, I think it’s slightly nicer to return a list with two components result and error.

safety <- function(expr) {
  tryCatch(
    error = function(cnd) {
      list(result = NULL, error = cnd)
    },
    list(result = expr, error = NULL)
  )
}

str(safety(1 + 10))
#> List of 2
#>  $ result: num 11
#>  $ error : NULL
str(safety(abort("Error!")))
#> List of 2
#>  $ result: NULL
#>  $ error :List of 4
#>   ..$ message: chr "Error!"
#>   ..$ call   : NULL
#>   ..$ trace  :List of 3
#>   .. ..$ calls  :List of 31
#>   .. .. ..$ : language local({     args = commandArgs(TRUE) ...
#>   .. .. ..$ : language eval.parent(substitute(eval(quote(expr), envir)))
#>   .. .. ..$ : language eval(expr, p)
#>   .. .. ..$ : language eval(expr, p)
#>   .. .. ..$ : language eval(quote({     args = commandArgs(TRUE) ...
#>   .. .. ..$ : language eval(quote({     args = commandArgs(TRUE) ...
#>   .. .. ..$ : language do.call(rmarkdown::render, c(args[1], readRDS(arg"..
#>   .. .. ..$ : language (function (input, output_format = NULL, output_fil..
#>   .. .. ..$ : language knitr::knit(knit_input, knit_output, envir = envir..
#>   .. .. ..$ : language process_file(text, output)
#>   .. .. ..$ : language withCallingHandlers(if (tangle) process_tangle(gro..
#>   .. .. ..$ : language process_group(group)
#>   .. .. ..$ : language process_group.block(group)
#>   .. .. ..$ : language call_block(x)
#>   .. .. ..$ : language block_exec(params)
#>   .. .. ..$ : language in_dir(input_dir(), evaluate(code, envir = env, ne..
#>   .. .. ..$ : language evaluate(code, envir = env, new_device = FALSE, ke..
#>   .. .. ..$ : language evaluate::evaluate(...)
#>   .. .. ..$ : language evaluate_call(expr, parsed$src[[i]], envir = envir..
#>   .. .. ..$ : language timing_fn(handle(ev <- withCallingHandlers(withVis..
#>   .. .. ..$ : language handle(ev <- withCallingHandlers(withVisible(eval(..
#>   .. .. ..$ : language withCallingHandlers(withVisible(eval(expr, envir, ..
#>   .. .. ..$ : language withVisible(eval(expr, envir, enclos))
#>   .. .. ..$ : language eval(expr, envir, enclos)
#>   .. .. ..$ : language eval(expr, envir, enclos)
#>   .. .. ..$ : language str(safety(abort("Error!")))
#>   .. .. ..$ : language safety(abort("Error!"))
#>   .. .. ..$ : language tryCatch(error = function(cnd) {     list(result =..
#>   .. .. .. ..- attr(*, "srcref")= 'srcref' int [1:8] 2 3 7 3 3 3 2 7
#>   .. .. .. .. ..- attr(*, "srcfile")=Classes 'srcfilecopy', 'srcfile' <en..
#>   .. .. ..$ : language tryCatchList(expr, classes, parentenv, handlers)
#>   .. .. ..$ : language tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>   .. .. ..$ : language doTryCatch(return(expr), name, parentenv, handler)
#>   .. ..$ parents: int [1:31] 0 1 2 3 0 5 6 6 8 9 ...
#>   .. ..$ envs   :List of 31
#>   .. .. ..$ : chr "0x3a805a0"
#>   .. .. ..$ : chr "0x3a80450"
#>   .. .. ..$ : chr "0x3a80108"
#>   .. .. ..$ : chr "global"
#>   .. .. ..$ : chr "0x3a7fa78"
#>   .. .. ..$ : chr "0x3a7f5e0"
#>   .. .. ..$ : chr "0x3a817d8"
#>   .. .. ..$ : chr "0x3bed628"
#>   .. .. ..$ : chr "0x27c9b40"
#>   .. .. ..$ : chr "0x21a31b0"
#>   .. .. ..$ : chr "0x5146500"
#>   .. .. ..$ : chr "0x5149958"
#>   .. .. ..$ : chr "0x5148880"
#>   .. .. ..$ : chr "0x51487a0"
#>   .. .. ..$ : chr "0x518a3c0"
#>   .. .. ..$ : chr "0x51b5630"
#>   .. .. ..$ : chr "0x51b7f98"
#>   .. .. ..$ : chr "0x51ba5b8"
#>   .. .. ..$ : chr "0x5c7fc58"
#>   .. .. ..$ : chr "0x5c98c60"
#>   .. .. ..$ : chr "0x5c98b80"
#>   .. .. ..$ : chr "0x5c98870"
#>   .. .. ..$ : chr "0x5c982c0"
#>   .. .. ..$ : chr "0x5c98090"
#>   .. .. ..$ : chr "global"
#>   .. .. ..$ : chr "0x5c97e98"
#>   .. .. ..$ : chr "0x5c97d10"
#>   .. .. ..$ : chr "0x5de1098"
#>   .. .. ..$ : chr "0x5de0998"
#>   .. .. ..$ : chr "0x5de0650"
#>   .. .. ..$ : chr "0x5de0308"
#>   .. ..- attr(*, "class")= chr "rlang_trace"
#>   ..$ parent : NULL
#>   ..- attr(*, "class")= chr [1:3] "rlang_error" "error" "condition"

(This is closely related to purrr::safely(), a function operator, which we’ll come back to in Section 10.2.1.)

7.6.3 Resignal

As well as returning default values when a condition is signalled, handlers can be used to make more informative error messages. One simple application is to make a function that works like option(warn = 2) for a single block of code. The idea is simple: we handle warnings by throwing an error:

You could write a similar function if you were trying to find the source of an annyoing message.

7.6.4 Record

Another common pattern is to record conditions for later investigation. The new challenge here is that calling handlers are called only for their side-effects so we can’t return values, but instead need to modify some object in place.

What if you also want to capture errors? You’ll need to wrap the withCallingHandlers() in a tryCatch(). If an error occurs, it will be the last condition.

This is the key idea underlying the evaluate package which powers knitr: it captures every output into a special data structure so that it can be later replayed. As a whole, the evaluate package is quite a lot more complicated than the code here because it also needs to handle plots and text output.

7.6.5 No default behaviour

A final useful pattern is to signal a condition that doesn’t inherit from message, warning or error. Because there is no default behaviour, this means the condition has no effect unless the user specifically requests it. For example, you could imagine a logging system based on conditions:

When you call log() a condition is signalled, but nothing happens because it has no default handler:

To “activate” logging you need a handler that does something with the log condition. Below I define a record_log() function that will record all logging messages to a path:

You could even imagine layering with another function that allows you to selectively suppress some logging levels.

If you create a condition object by hand, and signal it with signalCondition(), cnd_muffle() will not work. Instead you need to call it with a muffle restart defined, like this:

Restarts are currently beyond the scope of the book, but I suspect will be included in the 3rd edition.

7.6.6 Exercises

  1. Create suppressConditions() that works like suppressMessages() and supressWarnings() but supresses everything. Think carefully about how you should handle errors.

  2. Compare the following two implementations of message2error(). What is the main advantage of withCallingHandlers() in this scenario? (Hint: look carefully at the traceback.)

  3. How would you modify the catch_cnds() definition if you wanted to recreate the original intermingling of warnings and messages?

  4. Why is catching interrupts dangerous? Run this code to find out.

7.7 Quiz answers

  1. error, warning, and message.

  2. You could use try() or tryCatch().

  3. tryCatch() creates exiting handlers which will terminate the execution of wrapped code; withCallingHandlers() creates calling handlers which don’t affect the execution of wrapped code.

  4. Because you can then capture specific types of error with tryCatch(), rather than relying on the comparison of error strings, which is risky, especially when messages are translated.


  1. But note that cat() requires an explicit trailing "\n" to print a new line.

  2. You can suppress the message with try(..., silent = TRUE).

  3. You can tell if the expression failed because the result will have class try-error.