9 Conditions

9.1 Introduction

Not all problems are unexpected. When writing a function, you can often anticipate potential problems (like a non-existent file or the wrong type of input). Communicating these problems to the user is the job of conditions such as errors (stop()), warnings (warning()), and messages (message()). Conditions are usually displayed prominently, in a bold font or coloured red depending on your R interface. You can tell them apart because errors always start with “Error” and warnings with “Warning message”.

Unexpected errors require interactive debugging to figure out what went wrong. Some errors, however, are expected, and you want to handle them automatically. In R, expected errors crop up most frequently when you’re fitting many models to different datasets, such as bootstrap replicates. Sometimes the model might fail to fit and throw an error, but you don’t want to stop everything. Instead, you want to fit as many models as possible and then perform diagnostics after the fact.

In R, there are three tools for handling conditions (including errors) programmatically:

  • try() gives you the ability to continue execution even when an error occurs.

  • tryCatch() lets you specify handler functions that control what happens when a condition is signalled.

  • withCallingHandlers() is a variant of tryCatch() that establishes local handlers, whereas tryCatch() registers exiting handlers. Local handlers are called in the same context as where the condition is signalled, without interrupting the execution of the function. When an exiting handler from tryCatch() is called, the execution of the function is interrupted and the handler is called. withCallingHandlers() is rarely needed, but is useful to be aware of.

The following sections describe these tools in more detail.

Condition handling tools, like withCallingHandlers(), tryCatch(), and try() allow you as a user, to take specific actions when a condition occurs. For example, if you’re fitting many models, you might want to continue fitting the others even if one fails to converge. R offers an exceptionally powerful condition handling system based on ideas from Common Lisp, but it’s currently not very well documented or often used. This chapter will introduce you to the most important basics, but if you want to learn more, I recommend the following two sources:

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 function do you use to ignore errors in block of code?

  2. Why might you want to create an error with a custom S3 class?

9.1.1 Prerequisites

9.2 Signalling conditions

Collectively messages, warnings, and errors are known as conditions, and creating and sending them to the user is known as signalling. stop(), warning(), message().

Also interrupts.

To help better understand conditions and the underlying object that defines their behaviour we will use rlang::catch_cnd(). This takes a block of code and returns the first condition signalled, or NULL.

9.2.1 Errors

Fatal errors are raised by stop() and force all execution to terminate. Errors are used when there is no way for a function to continue.

Style: http://style.tidyverse.org/error-messages.html

To learn more about the internal construction of the object, we need to capture it:

This shows us that the error object has class inherits from “condition”. And it has two components: the error message, and the call from which the error occured.

The call is often not useful, so I think it’s good practice to use call. = FALSE

Something about rlang errors and capturing the traceback (when that actually works).

9.2.2 Warnings

Warnings are weaker than errors: they signal that something has gone wrong, but the code has been able to recover and continue. They are generated by warning().

By defaults, warnings are cached and printed only when control returns to the top level.

You can override this setting in two ways:

  • To control someone else’s warnings, set options(warn = 1)
  • To control your own warnings, set immediate. = TRUE

Warning objects are very similar to error objects. They have message and call, and are inherit from the condition class.

You should be cautious with your use of warnings(): 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 incorrect input. Reserve warnings for when you’re almost sure that the result is correct, but there’s something the user really should know. A good use of warnings is for deprecation: the code works, but will not work in the future, or generally a better method is available.

Base R tends to use warnings when only part of a vectorised input is invalid. However, I don’t find these warnings terrifically informative: they don’t tell you where the problem lies in the vector, and when embedded inside other code, it is challenging to figure the source of the warning. In fact, usually the best technique is to turn warnings into errors with options(warn = 2). Then you can use your existing error diagnosis skills.

9.2.3 Messages

Messages are generated by message() and are used to give informative output in a way that can easily be suppressed by the user (?suppressMessages()). I often use messages to let the user know what value the function has chosen for an important missing argument.

Messages are also important when developing packages. you need to print messages during startup, use `packageStartupMessage(): that ensures library(yourpackage, quietly = TRUE) hides all your messages too.

9.2.4 Printed output

Function authors can also communicate with their users with print() or cat(), but I think that’s a bad idea because it’s hard to capture and selectively ignore this sort of output. Printed output is not a condition, so you can’t use any of the useful condition handling tools you’ll learn about below.

Generally, you should use message() rather than cat() or print() for informing the user about actions that your function has taken. This is useful, for example, if you’ve had to do non-trivial computation to determine the default value of an argument, and you want to let the user know exactly what you’ve done.

9.2.5 Interrupts

Interrupts can’t be generated directly by the programmer, but are raised when the user attempts to terminate execution by pressing Ctrl + Break, Escape, or Ctrl + C (depending on the platform).

9.3 Ignoring conditions

Simplest way of handling conditions in R is to simply ignore them. These are the bluntest instruments, but can be convenient.

9.3.1 Ignoring errors

try() allows execution to continue even after an error has occurred. For example, 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 printed but execution will continue:

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

You can also capture the output of the try() function. If successful, it will be the last result evaluated in the block (just like a function). If unsuccessful it will be an (invisible) object of class “try-error”.

Generally, however, you should avoid switching between different behaviours based on the result of try(). Instead use tryCatch(), as described below. A useful try() pattern is to do assignment inside: this lets you define a default value to be used if the code does not succeed.

9.3.2 Silencing messages and warnings

There are two functions that are sort of analagous to try() for warnings() and messages(): suppressWarnings() and suppressMessages(). These allow you to suppress all warnings and messages generated by a block of code.

Be aware that these functions are fairly heavy handed: you can’t use them to suppress a single warning that you know about, while allowing other warnings that you don’t know about to pass through.

The implementation of these functions are complex because they rely on the restart system. This is basically the only use of the restart system in base R (or pretty much any package) so we don’t discuss here.

9.4 Condition handlers

tryCatch() and withCallingHandlers() are general tool for handling conditions. They allows you to map conditions to handlers, functions that are called with the condition as an input.

tryCatch() and withCallingHandlers() differ in the type of handlers they define;

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

  • withCallingHandlers() defines in-place handlers; after the condition is captued control returns to the context where the condition was signalled. This makes it most suitable for working with warnings(), messages(), and other conditions.

9.4.1 Exiting handlers

If a condition is signalled, tryCatch() will call the first handler whose name matches one of the classes of the condition. The names useful for built-in conditions are error, warning, message, interrupt, and the catch-all condition.

A handler function can do anything, but typically it will either return a value or create a more informative error message. For example, the show_condition() function below sets up handlers that return the type of condition signalled:

9.4.2 In-place handlers

The primary difference from tryCatch() is execution continues normally when the handler returns. This includes the signalling function which continues its course after having called the handler (e.g., stop() will continue stopping the program and message() or warning() will continue signalling a message/warning).

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 (e.g., deleting files, 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.

9.4.4 Exercises

  1. Read the source code for catch_cnd() and explain how it works.

  2. How could you rewrite show_condition() to use a single handler.

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

9.5 Use cases

What can you do with this tools? The following section exposes some come use cases.

9.5.1 Replacement value

You can use tryCatch() to implement try(). A simple implementation is shown below. base::try() is more complicated in order to make the error message look more like what you’d see if tryCatch() wasn’t used. Note the use of conditionMessage() to extract the message associated with the original error.

9.5.2 Resignal

As well as returning default values when a condition is signalled, handlers can be used to make more informative error messages. For example, by modifying the message stored in the error condition object, the following function wraps read.csv() to add the file name to any errors:

Update to use whatever rethrow() becomes.

9.5.3 Record

This is what the evaluate package does. It powers knitr. (A little more complicated because it also has to handle output which uses a different system.)

9.5.5 Muffle

Due to the way that restarts are implemented in R, the ability to muffle, or ignore a condition (so it doesn’t bubble up to other handlers) is defined by the function that signals the condition. message() and warning() automatically setup muffle handlers, but signalCondition() does not.

cnd_signal() ensures that a muffler is always set up. cnd_muffle(c) always picks the right muffler depending on the class of the condition.

Log messages to disk example.

9.6 Custom condition classes

One of the challenges of error handling in R is that most functions just call stop() with a string. That means if you want to figure out if a particular error occurred, you have to look at the text of the error message. This is error prone, not only because the text of the error might change over time, but also because many error messages are translated, so the message might be completely different to what you expect.

There are two reasons to create your own conditions:

  • To make it easier to test your own code. Rather than relying on string matching on the text of the error, you can perform richer comparisons.

  • To make it easier for the user to take different actions for different types of errors.

For example, “expected” errors (like a model failing to converge for some input datasets) can be silently ignored, while unexpected errors (like no disk space available) can be propagated to the user.

Base R doesn’t make it easier to create your own classed conditions but the rlang equivalents provide some hlpers.

abort(), warn(), inform().

(Note that you can define a method for the conditionMessage() message generic instead of generating a message at creation time. This is usually of limited utility. )

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

9.7 Quiz answers

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

  2. 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.