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

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

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

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” or “Warning message”, and messages with nothing.

stop("This is what an error looks like")
#> Error in eval(expr, envir, enclos): This is what an error looks like

warning("This is what a warning looks like")
#> Warning: This is what a warning looks like

message("This is what a message looks like")
#> This is what a message looks like

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

8.2.1 Errors

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

f <- function() g()
g <- function() h()
h <- function() stop("This is an error!")

f()
#> Error in h(): This is an error!

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. = FALSE33:

h <- function() stop("This is an error!", call. = FALSE)
f()
#> Error: This is an error!

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.

h <- function() abort("This is an error!")
f()
#> Error: This is an error!

(NB: 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 Section 8.5.)

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.

8.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:

fw <- function() {
  cat("1\n")
  warning("W1")
  cat("2\n")
  warning("W2")
  cat("3\n")
  warning("W3")
}

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

fw()
#> 1
#> 2
#> 3
#> Warning messages:
#> 1: In f() : W1
#> 2: In f() : W2
#> 3: In f() : W3

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 options(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:

formals(1)
#> Warning in formals(fun): argument is not a function
#> NULL

file.remove("this-file-doesn't-exist")
#> Warning in file.remove("this-file-doesn't-exist"): cannot remove file 'this-
#> file-doesn't-exist', reason 'No such file or directory'
#> [1] FALSE

lag(1:3, k = 1.5)
#> Warning in lag.default(1:3, k = 1.5): 'k' is not an integer
#> [1] 1 2 3
#> attr(,"tsp")
#> [1] -1  1  1

There are 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.

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

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

fm <- function() {
  cat("1\n")
  message("M1")
  cat("2\n")
  message("M2")
  cat("3\n")
  message("M3")
}

fm()
#> 1
#> M1
#> 2
#> M2
#> 3
#> M3

Good places to use a message are:

  • When a default argument requires 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 to 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 to 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 similar34:

cat("Hi!\n")
#> Hi!

message("Hi!")
#> Hi!

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.

8.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()?


  1. The trailing . in call. is a peculiarity of stop(); don’t read anything into it.↩︎

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