Conditions and Error Handling
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Everything on this page is base R (the base and utils packages that ship with R) except one short section on rlang 1.3.0 and cli 3.6.6, the two add-on packages that were installed for that example; the lintr 3.4.0 default linters were used only to check the scripts. R does not have exceptions in the sense of Java or Python. It has conditions: objects that describe something unusual, a mechanism that offers them to handlers while the code that signalled them is still running, and a second mechanism, restarts, that lets a handler choose how the interrupted code should go on. Errors, warnings and messages are the three everyday kinds, and they are all handled by the same few functions. This page is about those functions: what a condition is, how to signal one (including conditions of your own with extra data), the difference between handlers that unwind the stack (tryCatch()) and handlers that run on top of it (withCallingHandlers()), how restarts let the handler and the signaller cooperate, and the clean-up, warning and top-level behaviour that surrounds all of this, with the real messages R prints.
Every example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts also pass the default lintr linters; a # nolint: comment marks a line where the idiom on show is what a default linter dislikes, so the linter does not hide the lesson. The names books, loans, shelf_today, loan_days and the book() constructor and validate_book() validator come from the running library-shelf scenario described on the section index: a script that needs them starts with source("scenario.R"). Messages, warnings and the text of unhandled errors are written to standard error, not to standard output. The listings show them with the #> prefix at the position where R prints them at the console, right after the statement that produced them, so a deferred Warning message: appears after the value of the call that raised it. Where a message contains quotation marks, they are the typographic quotes R prints in a UTF-8 locale. Where the point is the failure itself, the script catches it with tryCatch() (or sets options(catch.script.errors = TRUE), explained below) so that the script still runs to the end. Debugging a failure after it happened is the subject of Debugging; checking arguments before they can fail is in Defensive Programming and Input Validation; what a function is and how on.exit() fits into it is in Functions.
What a condition is
A condition is a list with a class
The help page ?conditions states the model in a few sentences. Conditions are objects that carry information about what happened, such as a message and the call in which it happened; they inherit from the abstract class condition; errors and warnings inherit from the abstract subclasses error and warning; and they are currently S3-style objects, a representation that the help page says may eventually change. In practice a condition is a list with two elements, message and call, and a class attribute that is a vector of class names from the most specific to the most general. That is the same S3 machinery described in S3 Classes and Generics, and it explains everything else on this page: a handler is selected by looking for one of the names in the class vector, a custom condition is a condition with a longer class vector, and extra data is extra list elements.
simpleError(), simpleWarning(), simpleMessage() and simpleCondition() are the constructors for the plain cases. The first three are what stop(), warning() and message() use; simpleCondition() has no severity and is the starting point for conditions that are neither errors, warnings nor messages. conditionMessage() and conditionCall() are generic functions that return the two fields, which matters when a class defines its own method (see below). Printing a condition shows its class, the call after in when there is one, and the message; as.character() gives the text that the default handler would print, Error in <call>: <message> with a trailing newline.
# A condition is an S3 object: a list with a message and a call.
e <- simpleError("shelf is empty")
class(e)
#> [1] "simpleError" "error" "condition"
is.list(e)
#> [1] TRUE
str(unclass(e))
#> List of 2
#> $ message: chr "shelf is empty"
#> $ call : NULL
conditionMessage(e)
#> [1] "shelf is empty"
conditionCall(e)
#> NULL
e
#> <simpleError: shelf is empty>
# The constructors take an optional call; print() shows it after "in".
e_call <- simpleError("no such book", call = quote(find_book("x")))
e_call
#> <simpleError in find_book("x"): no such book>
conditionCall(e_call)
#> find_book("x")
as.character(e_call)
#> [1] "Error in find_book(\"x\"): no such book\n"
as.character(e)
#> [1] "Error: shelf is empty\n"
# simpleWarning, simpleMessage and simpleCondition follow the same pattern.
class(simpleWarning("careful"))
#> [1] "simpleWarning" "warning" "condition"
class(simpleMessage("note\n"))
#> [1] "simpleMessage" "message" "condition"
class(simpleCondition("a plain condition"))
#> [1] "simpleCondition" "condition"
inherits(e, "condition")
#> [1] TRUE
inherits(e, "error")
#> [1] TRUE
inherits(e, "warning")
#> [1] FALSE
Source: R documentation: conditions
The class vector is the whole type system. simpleError is the most specific class, error says "this is an error" and is what tryCatch(error = ) looks for, and condition is the root that every handler of the form condition = sees. A simple error is not a warning (inherits(e, "warning") is FALSE for the error above). Severity is only a convention of the class vector: warning() signals whatever condition object it is given, so the class decides which handlers see it, not the function that signalled it.
| Constructor | Class vector | Usually signalled by | Default action when no handler takes it |
|---|---|---|---|
|
|
|
The message is printed and the evaluation is abandoned: back to the top level in an interactive session, the end of the script (exit status 1) with |
|
|
|
Stored and printed after the top-level call completes, or printed at once, or turned into an error, depending on |
|
|
|
The text is written to standard error and the evaluation goes on |
|
|
|
None: |
|
the given |
|
As for an error |
|
the given |
|
As for a warning |
|
the given |
|
As for a message |
Source: R documentation: conditions
There is a fourth family in the help page that is not signalled by code you write: a user interrupt (the Control-C or Escape key) signals a condition of class interrupt, which inherits directly from condition, before the default interrupt action runs. tryCatch(expr, interrupt = function(i) ...) can intercept it, and suspendInterrupts() and allowInterrupts() mark stretches of code, typically clean-up code, in which interrupts must not (or must) be processed.
What R signals itself
The errors and warnings that R raises from its own code are conditions too, built from the same pieces. The call stored in the condition is the call of the function that signalled it, and that is why the messages R prints say Error in log(-1) or Warning in sqrt(-1). Two details of the next script deserve attention. A primitive such as as.integer() has no frame of its own, so a warning raised by it reports the closest enclosing closure call; and a stop() written directly inside tryCatch() reports a call that belongs to the machinery inside tryCatch(), doTryCatch(return(expr), name, parentenv, handler). Neither is a bug; both are why the call of a condition is documentation for people and not something a program should parse.
# What R itself signals is built from the same pieces.
failure <- function() stop("the loan file is missing")
cnd <- tryCatch(failure(), error = identity)
cnd
#> <simpleError in failure(): the loan file is missing>
class(cnd)
#> [1] "simpleError" "error" "condition"
conditionCall(cnd)
#> failure()
# call. = FALSE leaves the call out.
quiet_failure <- function() stop("the loan file is missing", call. = FALSE)
cnd <- tryCatch(quiet_failure(), error = identity)
cnd
#> <simpleError: the loan file is missing>
conditionCall(cnd)
#> NULL
# Internal errors and warnings are conditions too, with the call of the
# function that signalled them.
tryCatch(1 + "a", error = identity)
#> <simpleError in 1 + "a": non-numeric argument to binary operator>
tryCatch(log(-1), warning = identity)
#> <simpleWarning in log(-1): NaNs produced>
tryCatch(sqrt("a"), error = conditionCall)
#> sqrt("a")
# A condition raised directly inside tryCatch() (or a primitive such as
# as.integer(), which has no frame of its own) reports the closest function
# call, and that is part of tryCatch()'s own machinery.
tryCatch(stop("direct"), error = conditionCall)
#> doTryCatch(return(expr), name, parentenv, handler)
tryCatch(as.integer("x"), warning = conditionCall)
#> doTryCatch(return(expr), name, parentenv, handler)
Source: R documentation: stop
stop("...", call. = FALSE) drops the call, which is the right choice for an error that is addressed to the user of a function and not to its author: the message then starts with Error: and contains nothing but the explanation. The default call. = TRUE is better when the function is deep inside a package and the reader of the message needs to know where it came from. The shelf’s validate_book() uses call. = FALSE for exactly that reason.
Signalling a condition
stop(), warning() and message()
stop(...), warning(...) and message(...) take any number of arguments, convert them to character and paste them together with no separator, so stop("invalid year: ", 2200L) and stop(paste0("invalid year: ", 2200L)) are the same call. The three functions differ in what they promise. stop() never returns: once the handlers have had their say and none has taken a non-local exit, the default error action runs. warning() returns its message as a character string, invisibly, when the evaluation goes on. message() also returns when the evaluation goes on (its value is NULL, invisibly), and unlike the other two its text is not a sentence but a line: a final newline is regarded as part of the message, and message() adds one unless appendLF = FALSE. That is why the condition message of message("one line") is "one line\n".
The remaining arguments are small but useful. call. = FALSE (for stop() and warning()) leaves the call out of the condition. domain says where the translation of the message comes from; domain = NA switches translation off, and the help page’s advice is to use it whenever the arguments contain a result from gettextf(), because gettextf() has already translated its text and a second translation attempt would only look up a string that is not in any catalogue. warning(immediate. = TRUE) prints that warning at once even if options(warn) is 0, and warning(noBreaks. = TRUE) asks, with warn = 1, for the message to stay on a single line as far as possible (an argument that is new in R 3.1.0). Long messages are cut: the text of errors and warnings is truncated to getOption("warning.length") bytes, 1000 by default, and a truncated warning ends in [... truncated]. ngettext() is the function to use for messages that depend on a count ("1 loan" and "2 loans"); it is documented with gettext(), and gettextf() with sprintf(). The language of R’s own messages can be switched with Sys.setLanguage() (R 4.2.0), which is the reason code should never test the wording of a message (see Encodings, Locales and Internationalization).
# stop(), warning() and message() paste their arguments with no separator.
tryCatch(stop("invalid year: ", 2200L, " (max ", 2100L, ")"),
error = conditionMessage)
#> [1] "invalid year: 2200 (max 2100)"
tryCatch(warning("loans: ", 3, " overdue"), warning = conditionMessage)
#> [1] "loans: 3 overdue"
# A message ends with a newline: message() appends one, appendLF = FALSE not.
tryCatch(message("one line"), message = conditionMessage)
#> [1] "one line\n"
tryCatch(message("no newline", appendLF = FALSE), message = conditionMessage)
#> [1] "no newline"
# With no handler, message() writes to standard error and goes on.
message("loading the shelf")
#> loading the shelf
cat("the script continues\n")
#> the script continues
# warning() returns its message invisibly; stop() never returns.
msg <- suppressWarnings(warning("returned text"))
msg
#> [1] "returned text"
Source: R documentation: message
Source: R documentation: warning
Source: R documentation: gettext
Source: R documentation: sprintf
The second block of the listing shows the default action of message(): with no handler, the text goes to standard error and the script continues. That makes message() the right channel for diagnostics, progress and "I did something you did not ask for" notes, and cat() or print() the right channel for results. A message can be silenced by the caller with suppressMessages() or turned into data by a handler, which cat() cannot.
signalCondition()
stop(), warning() and message() are conveniences built on one primitive, signalCondition(cond), which looks for handlers for cond and calls them. The help page for stop() says that an error is first signalled as if by signalCondition(), and ?warning says the same for warnings: the severity-specific part is what happens after the handlers have declined (the default action), not how the handlers are found. Used directly, signalCondition() is the way to announce something that has no default action: if no handler takes the condition, it returns NULL and the code after it runs. A condition of a class such as shelf_note, which is neither an error nor a warning nor a message, is therefore completely optional: nobody has to handle it, and nothing happens when nobody does.
# signalCondition() looks for handlers; with none it returns NULL and goes on.
cnd <- simpleCondition("a note", call = NULL)
class(cnd) <- c("shelf_note", "condition")
signalCondition(cnd)
#> NULL
cat("signalCondition() returned\n")
#> signalCondition() returned
withCallingHandlers(
{
signalCondition(cnd)
cat("and the code after it ran\n")
},
shelf_note = \(n) cat("handler saw:", conditionMessage(n), "\n")
)
#> handler saw: a note
#> and the code after it ran
Source: R documentation: conditions
Because the handler for shelf_note returned, the search went on to the next matching handler; there was none, so signalCondition() itself returned NULL and the code after it ran. That is the difference to stop(): a calling handler that returns does not stop an error, it just passes it on.
Passing a condition object
All three functions also accept a condition object instead of text, and then the object is signalled as it is: its class vector, its message, its call and its extra fields reach the handlers unchanged. This is how classed conditions are raised (the next section builds them with errorCondition()). The condition has to be the only argument. The help pages say that further arguments are ignored, with a warning or a message; the check on R 4.6.1 below did something different: when a second argument is present, stop() builds a character string from all of them (the condition is converted with as.character(), which gives Error: an error built by hand followed by a newline) and signals a plain simpleError with that text. Pass the condition alone.
stopifnot() is a convenience for the most common check: it signals a simpleError whose call is the function that contains the check, with a message that names the failed expression or, for a named argument (R 4.0.0), the text you chose. It is the subject of Defensive Programming and Input Validation, and the last two examples of the listing show it and the domain = NA rule.
# stop(), warning() and message() also accept a condition object.
err <- simpleError("an error built by hand")
tryCatch(stop(err), error = \(e) identical(e, err))
#> [1] TRUE
wrn <- simpleWarning("a warning built by hand")
tryCatch(warning(wrn), warning = \(w) class(w))
#> [1] "simpleWarning" "warning" "condition"
msg_cnd <- simpleMessage("a message built by hand\n")
tryCatch(message(msg_cnd), message = \(m) class(m))
#> [1] "simpleMessage" "message" "condition"
# The condition has to be the only argument. With another one, R builds a
# text from both and signals a plain simpleError.
tryCatch(stop(err, "extra"), error = \(e) conditionMessage(e))
#> [1] "Error: an error built by hand\nextra"
# stopifnot() signals a simpleError whose call is the function that checked.
check_year <- function(year) {
stopifnot("year must be a whole number" = year == round(year))
year
}
tryCatch(check_year(1999.5), error = identity)
#> <simpleError in check_year(1999.5): year must be a whole number>
# Translated text: gettextf() has translated already, so domain = NA.
tryCatch(stop(gettextf("invalid ISBN-13: %s", "123"), domain = NA),
error = conditionMessage)
#> [1] "invalid ISBN-13: 123"
Source: R documentation: stopifnot
Conditions of your own
errorCondition(), warningCondition() and messageCondition()
Code that has to react differently to different failures should not read the message: the text is written for people, it can be translated, and a change of wording breaks every program that parsed it. The robust identity of a condition is its class, and the robust payload is a field. errorCondition(message, ..., class, call) builds an error condition whose class vector is class followed by error and condition; the ... arguments become extra elements of the list, and call defaults to NULL. warningCondition() is the same for warnings, and messageCondition() for messages. The first two arrived in R 3.6.0 (the NEWS describes them as a convenient way to create structured error and warning objects) and messageCondition() in R 4.6.0, completing the set. Message conditions are the least known of the three, and the useful ones: a function can report progress as a message that carries numbers (see the last example of this part), and the caller can log it, show it in a progress bar or silence it by class.
# errorCondition(), warningCondition() and messageCondition() build condition
# objects with your own class vector and extra fields. messageCondition() is
# new in R 4.6.0; the other two date from R 3.6.0.
cnd <- errorCondition(
"invalid ISBN-13: 123",
class = "shelf_invalid_isbn",
isbn = "123"
)
class(cnd)
#> [1] "shelf_invalid_isbn" "error" "condition"
str(unclass(cnd))
#> List of 3
#> $ message: chr "invalid ISBN-13: 123"
#> $ call : NULL
#> $ isbn : chr "123"
cnd$isbn
#> [1] "123"
wrn <- warningCondition(
"implausible year: 1066",
class = "shelf_odd_year",
year = 1066L
)
class(wrn)
#> [1] "shelf_odd_year" "warning" "condition"
msg <- messageCondition(
"checked 16 books\n",
class = "shelf_progress",
n = 16L
)
class(msg)
#> [1] "shelf_progress" "message" "condition"
msg
#> <shelf_progress: checked 16 books
#> >
Source: R documentation: conditions
Source: R NEWS: Changes in R 4.6.0
The class vector is built by the constructor, so class = "shelf_invalid_isbn" gives shelf_invalid_isbn, error, condition. It is possible to give several classes (class = c("shelf_invalid_isbn", "shelf_error")) to build a hierarchy of your own: a handler for shelf_error then takes every condition of the family. The convention that keeps the names from clashing with those of other packages is a prefix, the name of your package or project, followed by what happened; the shelf uses shelf_.
A classed error for the shelf
The shelf’s validate_book() signals plain errors with stop(..., call. = FALSE): their class is simpleError and only the text tells one failure from another. The wrapper check_book() below re-implements the ISBN check with a classed error (the page does not edit scenario.R; it defines the wrapper next to the example). The condition carries the offending value in the isbn field and the call of the wrapper, and a handler can use either the class or the field. Compare the last two calls: validate_book() signals the same failure as simpleError, so catching only the ISBN case without a class means comparing strings.
source("scenario.R")
# A wrapper around validate_book() that signals a classed condition. The text
# is the one validate_book() uses, but the class and the isbn field are new.
check_book <- function(x) {
isbn <- unclass(x)$isbn
if (!is.na(isbn) && !is_valid_isbn13(isbn)) { # nolint: object_usage_linter.
stop(errorCondition(
paste0("invalid ISBN-13: ", isbn),
class = "shelf_invalid_isbn",
isbn = isbn,
call = sys.call()
))
}
validate_book(x) # nolint: object_usage_linter.
}
bad <- new_book(
title = "Orchard Logic", author = "Test Author", isbn = "9780306406158",
year = 2020L, genre = "Fiction", pages = 100L
)
tryCatch(check_book(bad), shelf_invalid_isbn = \(e) e$isbn)
#> [1] "9780306406158"
tryCatch(check_book(bad), error = identity)
#> <shelf_invalid_isbn in check_book(bad): invalid ISBN-13: 9780306406158>
# Matching on the class does not depend on the wording of the message. The
# plain error of validate_book() can only be told apart by its text.
tryCatch(validate_book(bad), error = \(e) class(e))
#> [1] "simpleError" "error" "condition"
untitled <- new_book(
title = "", author = "A", isbn = NA_character_,
year = 2020L, genre = "Fiction", pages = 1L
)
tryCatch(check_book(untitled), shelf_invalid_isbn = \(e) "isbn",
error = \(e) conditionMessage(e))
#> [1] "a book needs a title"
# Extra fields and a conditionMessage() method: the method builds the text.
conditionMessage.shelf_invalid_isbn <- function(c) {
paste0("invalid ISBN-13 [", c$isbn, "]")
}
tryCatch(check_book(bad), error = conditionMessage)
#> [1] "invalid ISBN-13 [9780306406158]"
Source: R documentation: conditions
Two points about the listing. The handler \(e) e$isbn reads a field, which is the reason to use a condition object: the information survives as data, with its type, and nobody has to parse invalid ISBN-13: 9780306406158 to get it back. And conditionMessage.shelf_invalid_isbn() is an ordinary S3 method on the generic conditionMessage(), so a class can compute its message from its fields; try() and the default error handler use it, and so does tryCatch(error = conditionMessage). Keeping the fields and the message consistent is the author’s job, and a method is the way to avoid storing the same fact twice.
A family of classes
A package with many failure modes benefits from one more level in the class vector: a family class that every condition of the package carries. A caller that wants to treat any failure of the shelf as a shelf problem catches shelf_error; a caller that cares about one failure catches its specific class, listed first. The helper below is the single place where the shelf builds its errors, so the class names, the family and the call are decided once. call = sys.call(-1L) makes the condition name the function that called the helper (lend()), which is where the problem was found, instead of the helper itself.
source("scenario.R")
# A helper that every function of the shelf uses to signal its errors. The
# class vector ends in a family class, "shelf_error", so one handler can take
# every error of the package; call = sys.call(-1) names the caller of the
# helper, not the helper itself.
abort_shelf <- function(message, class = NULL, ...) {
stop(errorCondition(
message,
...,
class = c(class, "shelf_error"),
call = sys.call(-1L)
))
}
lend <- function(isbn, member) {
if (!nzchar(member)) {
abort_shelf("a loan needs a member", class = "shelf_no_member")
}
if (!is_valid_isbn13(isbn)) { # nolint: object_usage_linter.
abort_shelf(
paste0("invalid ISBN-13: ", isbn),
class = "shelf_invalid_isbn", isbn = isbn
)
}
paste(member, "borrowed", isbn)
}
# One handler for the family; the class vector shows the hierarchy.
tryCatch(lend("123", "Ana Ruiz"), shelf_error = class)
#> [1] "shelf_invalid_isbn" "shelf_error" "error"
#> [4] "condition"
tryCatch(lend("9780306406157", ""), shelf_error = class)
#> [1] "shelf_no_member" "shelf_error" "error" "condition"
# A more specific handler listed first takes its own case, the family handler
# the rest.
describe <- function(isbn, member) {
tryCatch(
lend(isbn, member),
shelf_invalid_isbn = \(e) paste("bad ISBN", e$isbn),
shelf_error = \(e) paste("shelf problem:", conditionMessage(e))
)
}
describe("123", "Ana Ruiz")
#> [1] "bad ISBN 123"
describe("9780306406157", "")
#> [1] "shelf problem: a loan needs a member"
describe("9780306406157", "Ana Ruiz")
#> [1] "Ana Ruiz borrowed 9780306406157"
# The call of the condition is the call of lend(), where the problem was found.
tryCatch(lend("123", "Ana Ruiz"), error = conditionCall)
#> lend("123", "Ana Ruiz")
Source: R documentation: conditions
As the listing shows, the class vector reads from the specific to the general, so the same condition satisfies shelf_invalid_isbn, shelf_error, error and condition handlers. R itself uses the same design: subscriptOutOfBoundsError and packageNotFoundError (see below) are errors with a more specific class in front of error.
The old way
Before errorCondition() the same object was built by hand with structure(), a list of message and call plus any extra elements and a class vector ending in error (or warning, message) and condition. The result is the same kind of object, still works, and appears in a lot of older code and tutorials. The only reason to prefer the constructors today is that they say what they mean and make it harder to forget the last two class names.
# Before errorCondition() the same object was built by hand with structure().
by_hand <- structure(
class = c("shelf_by_hand", "error", "condition"),
list(message = "built with structure()", call = NULL, detail = 42L)
)
tryCatch(stop(by_hand), shelf_by_hand = \(e) e$detail)
#> [1] 42
Source: R documentation: structure
Progress messages that carry data
messageCondition() solves a problem that used to need a package: reporting progress from deep inside a function without deciding how it is shown. The function signals a classed message with the numbers as fields; with no handler the text is printed on standard error; a caller that wants a progress bar, a log line or a counter installs a calling handler for the class, reads the fields and muffles the message; and a caller that wants to stop at the first report uses an exiting handler. The muffleMessage restart, explained below, is what stops the default printing. Because the message is also a condition of class message, suppressMessages() silences it with no extra code.
source("scenario.R")
# messageCondition() (R 4.6.0) lets a message carry data. A function can
# report progress as classed messages; the caller decides what to do with them.
progress <- function(done, total) {
message(messageCondition(
sprintf("checked %d of %d books\n", done, total),
class = "shelf_progress", done = done, total = total
))
}
check_all <- function() {
n <- nrow(books) # nolint: object_usage_linter.
ok <- logical(n)
for (i in seq_len(n)) {
ok[[i]] <- is_valid_isbn13(books$isbn[[i]]) # nolint: object_usage_linter.
if (i %% 8L == 0L) progress(i, n)
}
sum(ok)
}
# With no handler the default action prints the text on standard error.
check_all()
#> checked 8 of 16 books
#> checked 16 of 16 books
#> [1] 14
# A calling handler can use the fields and then muffle the message.
seen <- list()
valid <- withCallingHandlers(
check_all(),
shelf_progress = \(m) {
seen[[length(seen) + 1L]] <<- c(done = m$done, total = m$total)
invokeRestart("muffleMessage")
}
)
valid
#> [1] 14
do.call(rbind, seen)
#> done total
#> [1,] 8 16
#> [2,] 16 16
# An exiting handler stops the work at the first report.
tryCatch(check_all(), shelf_progress = \(m) m$done)
#> [1] 8
Source: R NEWS: Changes in R 4.6.0
Which kind of condition?
The three severities are a statement about what the caller can do, not about how bad the situation is.
| Situation | Signal | Reason |
|---|---|---|
The function cannot do what was asked, and any value it returned would be wrong |
An error: |
The caller must not carry on with a wrong value. The default action stops the evaluation. |
The function returns a usable value, but something probably surprising happened (coercion to |
A warning: |
The caller can classify the warning, count it, or turn it into an error with |
Progress, or a step the user did not request explicitly |
A message: |
It goes to standard error and does not mix with the result, and |
An event that others may want to observe and that needs no reaction |
|
Nothing happens when nobody listens. |
A failure that the application can repair in more than one way |
A classed condition signalled inside |
The caller picks the policy with a calling handler and |
Source: R documentation: conditions
Two habits make conditions easier to use. Write the message for the person who reads it: say what was wrong with which value (invalid ISBN-13: 123), not that "an error occurred", and leave the call out (call. = FALSE) when the reader is a user of the function and the call would only show internals. And do not use print() or cat() to report problems, because neither can be caught, classified or silenced by the caller.
Exiting handlers: tryCatch()
Handlers and finally
tryCatch(expr, ..., finally) evaluates expr with the handlers given in ... established. Each handler is an argument whose name is a condition class and whose value is a function of one argument, the condition. If nothing is signalled, tryCatch() returns the value of expr. If a condition is signalled, the handlers are searched from the most recently established one, and when several are given in a single call, the first one counts as more recent than the second. When a handler matches, control is transferred to the tryCatch() call that established it: the stack between the signalling code and tryCatch() is unwound (the functions in between are left, their on.exit() code runs, the code after the signalling call is never executed), the handler is called with the condition, and the value of the handler is the value of the tryCatch() call. That is why such a handler is called exiting: it is entered through an exit.
Two consequences are the most frequent reasons for confusion. The first is the order of the handlers. A condition of class simpleError, error, condition is matched against the handlers in the order they are written and the first match wins, even if a later handler is more specific, so condition = before error = makes the error handler dead code. The second is that the handler decides the value. A warning handler that returns "gave up" replaces the value that the code would have produced, and the statements after the warning() call do not run; if the aim is to record the warning and still get the value, use a calling handler (next section).
finally is an expression evaluated after the expression and the handlers, whatever happened. The help page specifies that it is evaluated in the context in which tryCatch() was called, so that the handlers of the current call are no longer active while it runs, and that its value is ignored. It is the place for code that must run on every path, such as closing a connection; on.exit() serves the same purpose for a function body.
# Without a condition tryCatch() returns the value of the expression; the
# finally expression runs either way and does not change the value.
tryCatch(21 * 2, error = \(e) "failed", finally = cat("finally ran\n"))
#> finally ran
#> [1] 42
tryCatch(stop("x"), error = \(e) "failed", finally = cat("finally ran\n"))
#> finally ran
#> [1] "failed"
# The value of the handler becomes the value of tryCatch().
tryCatch(warning("careful"), warning = \(w) toupper(conditionMessage(w)))
#> [1] "CAREFUL"
# Handlers are tried in the order they are written; the first one whose name
# is in the class vector of the condition wins.
tryCatch(stop("x"), condition = \(c) "condition", error = \(e) "error")
#> [1] "condition"
tryCatch(stop("x"), error = \(e) "error", condition = \(c) "condition")
#> [1] "error"
# A condition that matches no handler goes on to the outer handlers.
tryCatch(
tryCatch(stop("inner"), warning = \(w) "inner warning handler"),
error = \(e) paste("outer caught:", conditionMessage(e))
)
#> [1] "outer caught: inner"
# Once a handler has run, the code that signalled the condition is gone.
noisy <- function() {
warning("first warning")
cat("this line is never reached\n")
"value"
}
tryCatch(noisy(), warning = \(w) "gave up")
#> [1] "gave up"
Source: R documentation: conditions
A handler is not required to be an anonymous function: identity returns the condition itself, conditionMessage returns its text, and conditionCall its call, as the earlier listings did. A handler that is not a function fails when it is called, not when tryCatch() is set up, with attempt to apply non-function; a handler without a name fails at once with condition handlers must be specified with a condition class, which is what happens when the argument name is forgotten (tryCatch(expr, function(e) ...)).
Re-signalling and wrapping
A handler can decline to deal with a condition by signalling it again, which is how a function adds context or converts a low-level failure into one of its own. stop(e) with the caught object raises the same condition, class and fields included, so an outer handler for shelf_lost still sees it (second example below). If the handler instead calls stop("new text"), a new simpleError is created, and its call is the call of the machinery that invoked the handler, value[[3L]](cond) in the output below. That is the line in Error in value[[3L]](cond) : that people see in logs and wonder about: it is not the call that failed, it is the handler of a tryCatch() that failed. Passing call. = FALSE leaves it out, and building the new condition with errorCondition(), a class and the original condition as a field keeps the information in a form a program can use.
source("scenario.R")
# Handlers are ordinary functions: they can return the condition itself, and
# the caller can test its class.
res <- tryCatch(book("Valid", isbn = "123"), error = identity)
inherits(res, "error")
#> [1] TRUE
conditionMessage(res)
#> [1] "invalid ISBN-13: 123"
# Re-signalling: stop(e) raises the same object again, class and all.
rethrown <- tryCatch(
tryCatch(stop(errorCondition("lost", class = "shelf_lost")),
error = \(e) stop(e)),
shelf_lost = \(e) "still a shelf_lost"
)
rethrown
#> [1] "still a shelf_lost"
# Calling stop() inside a handler gives the new error the call of the handler
# machinery, not of your function.
tryCatch(
tryCatch(stop("inner"), error = \(e) stop("wrapped")),
error = conditionCall
)
#> value[[3L]](cond)
# Wrapping adds context; the new message is a new condition.
add_book <- function(isbn) {
tryCatch(
book("Valid", isbn = isbn), # nolint: object_usage_linter.
error = \(e) {
stop("cannot add to the shelf: ", conditionMessage(e), call. = FALSE)
}
)
}
tryCatch(add_book("123"), error = conditionMessage)
#> [1] "cannot add to the shelf: invalid ISBN-13: 123"
Source: R documentation: stop
Keeping a long loop going
R FAQ 7.32 asks how to "capture or ignore errors in a long simulation" and answers with try() and, "preferably", tryCatch(), where the return value can be configured more flexibly: its example returns the coefficients if the lm() call succeeded and a vector of NaN if it failed. The same pattern with the shelf is a loop that builds a book() from each row of books: two rows have an invalid ISBN, row 9 with an empty one and row 13 with a wrong check digit, and book() stops at the first problem of its row. The handler returns the condition itself, so the loop keeps going and the failures are data afterwards: which rows failed and why. Returning NULL or NA instead would keep the loop going too, but would throw the reason away.
source("scenario.R")
# R FAQ 7.32: keep going in a long loop and record what failed.
try_book <- function(i) {
tryCatch(
do.call(book, as.list(books[i, ])), # nolint: object_usage_linter.
error = \(e) e
)
}
results <- lapply(seq_len(nrow(books)), try_book)
failed <- vapply(results, inherits, logical(1L), what = "error")
sum(failed)
#> [1] 2
which(failed)
#> [1] 9 13
vapply(results[failed], conditionMessage, character(1L))
#> [1] "invalid ISBN-13: " "invalid ISBN-13: 9780011001129"
format(results[[1L]])
#> [1] "The Dispossessed (Ursula K. Le Guin, 1974)"
Source: R documentation: do.call
The list results mixes book objects and simpleError objects, and the vapply() call with inherits(what = "error") tells them apart. A function that has to run over many items and report at the end follows the same shape: catch, keep the condition, summarise. The functional-programming pages show the iteration side (The apply Family and Functionals).
Releasing resources with finally
The finally argument is the way to guarantee a clean-up that belongs to a block of code and not to a function. The connection in the example is closed whether readLines() succeeds or fails, and after that the connection object is no longer valid, which is what the last line shows. For clean-up that belongs to a function, on.exit() is the tool (below).
# finally is the place to release something the expression opened.
con <- textConnection("first line\nsecond line")
first <- tryCatch(readLines(con, n = 1L), finally = close(con))
first
#> [1] "first line"
tryCatch(isOpen(con), error = conditionMessage)
#> [1] "invalid connection"
Source: R documentation: textConnection
Calling handlers: withCallingHandlers()
The signaller carries on
withCallingHandlers(expr, ...) establishes handlers of a different kind. When a condition is signalled and the matching handler is a calling handler, the handler is called by signalCondition() in the context where the condition was signalled: all the frames of the signalling code are still on the stack, and nothing is unwound. The help page adds that, while the handler runs, only the handlers below it in the handler stack are available, so a condition signalled by the handler itself goes to the outer handlers, not to itself. If the handler returns, the next matching handler is tried, and when the last one has been tried signalCondition() returns NULL and the signalling code goes on, after which the default action for the condition applies: a warning is printed or stored, a message is written to standard error, an error ends the evaluation.
That makes calling handlers the right tool for observing (logging, counting, adding context) and for recovering through a restart, and the wrong tool for replacing a value. They cannot return a value in place of the failed expression; the value of the handler is ignored. A handler that wants to stop the default action has to take a non-local exit, either by calling invokeRestart() or by signalling something else (an error raised inside a calling handler is caught by outer tryCatch() handlers like any other).
# withCallingHandlers() runs the handler where the condition was signalled.
# When the handler returns, the code that signalled it carries on.
noisy <- function() {
warning("first warning")
cat("this line IS reached\n")
"value"
}
withCallingHandlers(
noisy(),
warning = \(w) cat("handler saw:", conditionMessage(w), "\n")
)
#> handler saw: first warning
#> Warning in noisy() : first warning
#> this line IS reached
#> [1] "value"
# The default action still follows (the warning is printed or stored) unless
# the handler muffles it with the restart that warning() set up.
muffled <- withCallingHandlers(
noisy(),
warning = \(w) {
cat("log:", conditionMessage(w), "\n")
invokeRestart("muffleWarning")
}
)
#> log: first warning
#> this line IS reached
muffled
#> [1] "value"
# Same for messages and the muffleMessage restart.
res <- withCallingHandlers(
{
message("step 1 done")
message("step 2 done")
"finished"
},
message = \(m) {
cat("[log]", conditionMessage(m))
invokeRestart("muffleMessage")
}
)
#> [log] step 1 done
#> [log] step 2 done
res
#> [1] "finished"
Source: R documentation: conditions
The first call shows the order of events in the console: the handler prints its line, the default action then prints the warning (it is Warning in noisy() : because options(warn = 1) is on), and the line after warning() in noisy() does run. The second and third calls muffle: invokeRestart("muffleWarning") and invokeRestart("muffleMessage") jump to restarts that warning() and message() establish around the signalling, and when invoked they make warning() or message() return at once, skipping the default action. The restart is the contract between the signaller and the handler: the function that signals promises a way to carry on, and the handler chooses to use it.
The handler stack
Handlers form a stack, searched from the newest. The next listing prints the call stack inside a calling handler, which makes the idea concrete: outer() and inner() are still active, and below them are the frames of warning() itself and of the restart that it set up (withRestarts(), withOneRestart(), doWithOneRestart()); the last element is the handler. The same listing shows that with two nested withCallingHandlers() calls both handlers run, the inner one first, as long as each one returns.
# The stack is still there while a calling handler runs.
inner <- function() warning("deep warning")
outer <- function() inner()
withCallingHandlers(
outer(),
warning = \(w) {
calls <- sys.calls()
print(vapply(calls, \(cl) deparse(cl[[1L]])[[1L]], character(1L)))
invokeRestart("muffleWarning")
}
)
#> [1] "withCallingHandlers" "outer" "inner"
#> [4] "warning" ".signalSimpleWarning" "withRestarts"
#> [7] "withOneRestart" "doWithOneRestart" "function (w) "
# Several handlers are tried from the most recent one outwards, and each one
# that returns hands over to the next.
withCallingHandlers(
withCallingHandlers(
{
warning("w")
cat("continued\n")
},
warning = \(w) cat("inner handler\n")
),
warning = \(w) {
cat("outer handler\n")
invokeRestart("muffleWarning")
}
)
#> inner handler
#> outer handler
#> continued
The frame list is also what traceback() and sys.calls() are made to inspect, and it is the reason a calling handler is the tool for logging the context of an error: it can print sys.calls() before anything is unwound. Debugging with it is covered in Debugging.
Details that matter
Three details come up often. While a calling handler runs, only the handlers below it in the stack are active, so the warning raised inside the inner handler in the first block reaches only the outer handler. When one call lists several handlers, all matching ones are called in the order written (the warning handler and then the condition handler for a warning), as long as each returns; a general condition handler must therefore not assume that the restart it wants exists, which is what tryInvokeRestart() (R 4.0.0) is for. The help page recommends it whenever the condition may come from a foreign context, because a condition of a given class can be signalled by any protocol: a warning object can be signalled with stop() or message(), which do not set up a muffleWarning restart. Calling invokeRestart("muffleWarning") there is an error; calling tryInvokeRestart("muffleWarning") returns silently.
# While a calling handler runs, only the handlers below it are active, so a
# warning signalled inside the handler goes to the outer handler, not to itself.
withCallingHandlers(
withCallingHandlers(
{
warning("original")
"end"
},
warning = \(w) {
cat("inner handler got:", conditionMessage(w), "\n")
warning("raised by the handler")
invokeRestart("muffleWarning")
}
),
warning = \(w) {
cat("outer handler got:", conditionMessage(w), "\n")
invokeRestart("muffleWarning")
}
)
#> inner handler got: original
#> outer handler got: raised by the handler
#> [1] "end"
# Several handlers in one call: all that match run, in the order written.
withCallingHandlers(
{
warning("w")
"done"
},
warning = \(w) cat("specific: warning\n"),
condition = \(c) {
cat("general: condition\n")
tryInvokeRestart("muffleWarning")
}
)
#> specific: warning
#> general: condition
#> [1] "done"
Source: R documentation: conditions
Collecting warnings without losing the value
A common request is "give me the result and the warnings that came with it". An exiting handler cannot do that, because it unwinds and replaces the value. A calling handler can: it records the warning in a variable of the enclosing function (with <<-, explained in Scoping, Environments and Closures) and muffles it, and the expression runs to its end. The helper below returns both. Note that as.integer() raises one warning for the whole vector, so a vector with two bad strings produces one entry.
# Collecting the warnings of a call without losing its value.
collect_warnings <- function(expr) {
seen <- character()
value <- withCallingHandlers(
expr,
warning = \(w) {
seen <<- c(seen, conditionMessage(w))
invokeRestart("muffleWarning")
}
)
list(value = value, warnings = seen)
}
collect_warnings(as.integer(c("1", "x", "3", "y")))
#> $value
#> [1] 1 NA 3 NA
#>
#> $warnings
#> [1] "NAs introduced by coercion"
# An exiting handler cannot do that: it replaces the value.
tryCatch(as.integer(c("1", "x", "3", "y")), warning = \(w) "no value left")
#> [1] "no value left"
Source: R documentation: warning
How a condition travels
The mechanisms above fit in one picture. A function signals a warning inside a withCallingHandlers() call that itself sits inside a tryCatch(). The calling handler is the newest handler, so it is called first, on top of the stack. From there the path forks: if the handler invokes the restart that warning() set up, control goes back to the signaller and the code goes on as if the warning had been a harmless remark; if it simply returns, the search continues to the next matching handler, the exiting handler of tryCatch(), and that one unwinds the stack. finally runs last on both paths.
Source: R documentation: conditions
If no handler matched at all, the search ends in the default action of the condition (step 4 would be followed by the printing of the warning, or the abort of an error), which the tables in this page list. The script below prints one line for each numbered step of the diagram, once with the restart and once without; the numbers in its output are those of the diagram. The on.exit() line shows when the unwinding of import_loans() happens relative to the exiting handler: the handler runs after the functions it left have run their clean-up code.
# The two outcomes of warning(), traced step by step. The calling handler is
# established inside the tryCatch(), so it is the most recent handler and
# runs first.
import_loans <- function() {
on.exit(cat(" on.exit of import_loans() runs\n"))
cat("1. import_loans() signals a warning\n")
warning("loan L020 has an impossible date")
cat("5. import_loans() continues after warning()\n")
"imported"
}
run <- function(muffle) {
tryCatch(
withCallingHandlers(
import_loans(),
warning = \(w) {
cat("2. calling handler runs; the stack is still there\n")
if (muffle) {
cat("3. it invokes the muffleWarning restart\n")
invokeRestart("muffleWarning")
}
cat("3. it returns, so the next handler is tried\n")
}
),
warning = \(w) {
cat("4. exiting handler runs after the unwind\n")
"recovered"
},
finally = cat("6. finally runs last\n")
)
}
cat("--- muffled\n")
#> --- muffled
run(muffle = TRUE)
#> 1. import_loans() signals a warning
#> 2. calling handler runs; the stack is still there
#> 3. it invokes the muffleWarning restart
#> 5. import_loans() continues after warning()
#> on.exit of import_loans() runs
#> 6. finally runs last
#> [1] "imported"
cat("--- not muffled\n")
#> --- not muffled
run(muffle = FALSE)
#> 1. import_loans() signals a warning
#> 2. calling handler runs; the stack is still there
#> 3. it returns, so the next handler is tried
#> on.exit of import_loans() runs
#> 4. exiting handler runs after the unwind
#> 6. finally runs last
#> [1] "recovered"
Source: R documentation: conditions
The contrast to remember is the one the help page draws. With a tryCatch() handler, control is transferred to the tryCatch() call that established it. With a calling handler, signalCondition() calls the handler in the context where the condition was signalled. The help page says that the tryCatch mechanism is similar to Java error handling, and that calling handlers are based on Common Lisp and Dylan, as are restarts, which are the next topic.
Choosing a mechanism
| I want to | Use |
|---|---|
Get a fallback value when an expression fails |
|
Keep a failure as data and go on with the next item |
|
Run code whatever happens in a block |
|
Run code whatever happens in a function |
|
Log or count a condition and let the code go on |
|
Keep the value of an expression and collect its warnings |
|
Let the caller decide how to recover |
|
Hide one expected warning or message |
|
Find where a warning comes from |
|
Record every unhandled error of a session |
|
Source: R documentation: conditions
Restarts
Establishing and invoking restarts
A restart is a named place in the code to which a handler can jump, together with a function that runs there. warning() establishes muffleWarning, message() establishes muffleMessage, and the top level has an abort restart that returns to the top level. computeRestarts(cond) lists the restarts that are available (when a condition is given, those that do not apply to it are left out), findRestart(name) returns the newest one with that name, and invokeRestart(r, ...) transfers control to the point where the restart was established and calls its function with the extra arguments; r may be a name, and when no restart has that name the call is an error (no 'restart' 'nope' found). tryInvokeRestart() is the permissive variant that returns invisibly. withRestarts(expr, ...) establishes restarts of your own, in the form name = function (the function is the restart handler), name = "description" (the handler ignores its arguments and returns NULL) or name = list(handler, description, test), where test is a function of the condition that says whether the restart applies to it. restartDescription() and restartFormals() read the description and the arguments.
The value of withRestarts() is the value of the restart function when one was invoked, which is how a handler can supply a value in place of the failed computation: it invokes a restart and passes the value as an argument. Inside an exiting handler, the stack has been unwound already, so the restarts established below the tryCatch() are gone: in the second example only abort remains, and invoking a restart that is not available, as the third call does with muffleWarning, is an error.
# Restarts are named places a handler can jump back to. warning() and
# message() establish one each.
withCallingHandlers(
warning("w"),
warning = \(w) {
print(computeRestarts(w))
invokeRestart("muffleWarning")
}
)
#> [[1]]
#> <restart: muffleWarning >
#>
#> [[2]]
#> <restart: abort >
# Inside an exiting handler the stack has already been unwound, so the
# restart is gone; tryInvokeRestart() then returns silently.
tryCatch(warning("w"), warning = \(w) {
print(computeRestarts(w))
tryInvokeRestart("muffleWarning")
})
#> [[1]]
#> <restart: abort >
tryCatch(invokeRestart("muffleWarning"), error = identity)
#> <simpleError in invokeRestart("muffleWarning"): no 'restart' 'muffleWarning' found>
# withRestarts() establishes restarts; invokeRestart() jumps to one and passes
# arguments to the restart function. Its value is the value of withRestarts().
withRestarts(invokeRestart("add", 1, 2), add = \(x, y) x + y)
#> [1] 3
withRestarts(
{
cat("before\n")
invokeRestart("skip")
cat("not reached\n")
},
skip = \(...) "skipped"
)
#> before
#> [1] "skipped"
# The restart can be described and inspected.
withRestarts(
{
r <- findRestart("use_value")
print(r)
print(restartDescription(r))
names(restartFormals(r))
},
use_value = list(
handler = \(value) value,
description = "use this value instead"
)
)
#> <restart: use_value >
#> [1] "use this value instead"
#> [1] "value"
Source: R documentation: conditions
Policy and mechanism
The reason restarts exist is that the code that knows how to recover is often not the code that knows whether to recover. A loader knows that a book without a valid ISBN can be skipped or given a placeholder, but not which of the two the application wants; the application knows, but is far away from the loader. With plain errors, the loader would have to take an argument for every policy. With restarts, the loader signals a classed condition inside withRestarts() that offers the ways out, and the caller chooses with a calling handler that invokes one of them. Only the frames between the signalling call and withRestarts() are unwound, so the loop in load_all() carries on with the next book, and the third policy shows the other option: not using a restart at all, so that the condition reaches an exiting handler.
source("scenario.R")
# The shelf: a loader that offers two ways to recover from a bad ISBN, and a
# caller that chooses. Mechanism and policy are separate.
parse_book <- function(i) {
isbn <- books$isbn[[i]] # nolint: object_usage_linter.
title <- trimws(books$title[[i]]) # nolint: object_usage_linter.
if (!is_valid_isbn13(isbn)) { # nolint: object_usage_linter.
withRestarts(
stop(errorCondition(
paste0("invalid ISBN-13: ", isbn),
class = "shelf_invalid_isbn", isbn = isbn
)),
skip_book = \() NULL,
use_isbn = \(value) paste(title, value)
)
} else {
paste(title, normalise_isbn(isbn)) # nolint: object_usage_linter.
}
}
load_all <- function() {
lapply(seq_len(nrow(books)), parse_book) # nolint: object_usage_linter.
}
# Policy 1: skip every bad book.
skipping <- withCallingHandlers(
load_all(),
shelf_invalid_isbn = \(e) invokeRestart("skip_book")
)
length(skipping)
#> [1] 16
sum(vapply(skipping, is.null, logical(1L)))
#> [1] 2
# Policy 2: substitute a placeholder ISBN and keep every book.
patched <- withCallingHandlers(
load_all(),
shelf_invalid_isbn = \(e) invokeRestart("use_isbn", "UNKNOWN")
)
sum(vapply(patched, is.null, logical(1L)))
#> [1] 0
patched[[9L]]
#> [1] "Rivers of Glass UNKNOWN"
patched[[13L]]
#> [1] "Orchard Logic UNKNOWN"
# Policy 3: give up at the first bad book (row 9, whose ISBN is empty).
tryCatch(load_all(), shelf_invalid_isbn = \(e) e$isbn)
#> [1] ""
Source: R documentation: conditions
The ISBN of the first failing row is empty (row 9), and the handler for the third policy returned it as the value of tryCatch(): an empty string. The pattern is the one of the Common Lisp restart mechanism, which the help page names as the model for restarts, and it is rare in R code; most packages stop at tryCatch() and the two built-in restarts. Its natural home is a library with a long-running operation (an importer, a downloader) where the right response to a bad item depends on the application.
try()
try(expr, silent = FALSE, outFile = getOption("try.outFile", default = stderr())) is the older and simpler interface. It evaluates expr; if an error occurs it prints the error message to standard error (unless silent = TRUE or options(show.error.messages = FALSE)) and returns, invisibly, an object of class "try-error". That object is the message as a character string, Error in <call> : <message> with a newline, and the condition itself is kept in the "condition" attribute. The text is also stored where geterrmessage() can read it. try() is implemented with tryCatch(), and the help page is direct about the consequence: for programming, a simple tryCatch(expr, error = function(e) e) may be more efficient and more flexible than try(expr, silent = TRUE). try() remains useful at the console and in scripts that only want to keep going, and in the examples of older code, which is why every reader should recognise the result.
The test for failure is inherits(res, "try-error"). The help page carries an explicit warning against if (class(res) == "try-error"): when there is no error the result may have a class vector longer than one (it is the value of the expression, whatever its class), the comparison then has length greater than one, and since R 4.2.0 a condition of length greater than one is an error. In the listing, a successful call returns its value unchanged and inherits() is FALSE.
One detail of the text is a surprise the first time. When stop() is called inside a function, the message names that function (load_loans("loans.csv") below). When stop() is written directly in the expression, the machinery of tryCatch() would be named (see above), and try() shows its own call instead: the outFile example near the end of the listing prints Error in try(stop("shown on stdout"), outFile = stdout()). With call. = FALSE the message starts with Error : (a space before the colon, because the call is empty).
# try() runs an expression, prints a failure to standard error and returns an
# object of class "try-error" instead of stopping.
load_loans <- function(path) {
stop("cannot open ", path)
}
res <- try(load_loans("loans.csv"))
#> Error in load_loans("loans.csv") : cannot open loans.csv
cat("the script continues\n")
#> the script continues
class(res)
#> [1] "try-error"
inherits(res, "try-error")
#> [1] TRUE
res
#> [1] "Error in load_loans(\"loans.csv\") : cannot open loans.csv\n"
#> attr(,"class")
#> [1] "try-error"
#> attr(,"condition")
#> <simpleError in load_loans("loans.csv"): cannot open loans.csv>
# The text is the message with the call in front; the condition is kept in an
# attribute.
cat(res)
#> Error in load_loans("loans.csv") : cannot open loans.csv
attr(res, "condition")
#> <simpleError in load_loans("loans.csv"): cannot open loans.csv>
conditionMessage(attr(res, "condition"))
#> [1] "cannot open loans.csv"
geterrmessage()
#> [1] "Error in load_loans(\"loans.csv\") : cannot open loans.csv\n"
# silent = TRUE suppresses the printing; the value is the same.
quiet <- try(load_loans("loans.csv"), silent = TRUE)
identical(quiet, res)
#> [1] TRUE
# With call. = FALSE the message has no call: "Error : ..." with a blank.
no_call <- try(stop("plain failure", call. = FALSE), silent = TRUE)
cat(no_call)
#> Error : plain failure
# The value of a successful expression is returned unchanged.
ok <- try(log(100, base = 10), silent = TRUE)
ok
#> [1] 2
inherits(ok, "try-error")
#> [1] FALSE
# Test with inherits(): class(res) == "try-error" would break as soon as the
# class vector has more than one element.
results <- lapply(c("books.csv", "loans.csv"), \(path) {
try(if (path == "loans.csv") stop("locked: ", path) else path, silent = TRUE)
})
failed <- vapply(results, inherits, logical(1L), what = "try-error")
failed
#> [1] FALSE TRUE
results[!failed]
#> [[1]]
#> [1] "books.csv"
# The message can be redirected to standard output.
try(stop("shown on stdout"), outFile = stdout())
#> Error in try(stop("shown on stdout"), outFile = stdout()) :
#> shown on stdout
# What try() is made of, and the usual replacement for programming.
equivalent <- tryCatch(load_loans("loans.csv"), error = identity)
conditionMessage(equivalent)
#> [1] "cannot open loans.csv"
Source: R documentation: try
outFile redirects the message, for example to stdout(), and options(try.outFile = stdout()) does that for the session, which the help page suggests for try() inside a Sweave chunk and for any document where the error text should appear in the output. Note that all the try() calls in the listing print their message at the point of the call, before the next line runs: the value is returned and the evaluation goes on, which is why the loop-over-items idiom works with try() too.
Cleaning up: on.exit()
on.exit(expr, add = FALSE, after = TRUE) records code to run when the current function exits, normally or because a condition ended it. The R Language Definition describes it in the chapter on exception handling: a function can insert a call to on.exit at any point of its body, the expression is stored, and it is executed when the function exits. The chapter adds that expressions installed with options(error = ) are evaluated before the on.exit() calls are carried out, which the section on unhandled errors below confirms. The general behaviour of on.exit() (the replacement rule of add = FALSE, the after argument, the expression that sees the variables as they are at exit) is described with examples in Functions; this page only adds what happens around it when conditions are involved.
The sequence when an error is raised inside a function that has an on.exit() handler, a calling handler outside it, and an exiting handler outside that, is the one that the previous diagram implies: the calling handler runs first, while the function is still on the stack; then the stack is unwound and on.exit() runs; then the exiting handler; then finally. The listing prints exactly that order. The R Language Definition says that an error in the evaluation of the on.exit code makes R jump to the top level without processing any further on.exit code; in the check on R 4.6.1 with add = TRUE, an error in the second of three expressions did not prevent the third from running (the error was caught by an outer tryCatch()), which agrees with the behaviour that the functions page describes. Do not rely on a failing clean-up step to be skipped, and keep clean-up code simple enough that it cannot fail.
# on.exit() runs when the function exits, whether it returns or fails.
fetch <- function(fail) {
on.exit(cat("on.exit: release the lock\n"))
cat("working\n")
if (fail) stop("fetch failed")
"result"
}
fetch(FALSE)
#> working
#> on.exit: release the lock
#> [1] "result"
tryCatch(fetch(TRUE), error = \(e) "recovered")
#> working
#> on.exit: release the lock
#> [1] "recovered"
# add = TRUE appends, after = FALSE puts the new expression in front.
steps <- function() {
on.exit(cat("A\n"))
on.exit(cat("B\n"), add = TRUE)
on.exit(cat("C\n"), add = TRUE, after = FALSE)
cat("body\n")
}
steps()
#> body
#> C
#> A
#> B
steps_expr <- function() {
on.exit(cat("A\n"))
on.exit(cat("B\n"), add = TRUE)
sys.on.exit()
}
steps_expr()
#> A
#> B
#> {
#> cat("A\n")
#> cat("B\n")
#> }
# One failing cleanup does not stop the others when add = TRUE was used.
cleanup <- function() {
on.exit(cat("first cleanup\n"), add = TRUE)
on.exit(stop("cleanup failed"), add = TRUE)
on.exit(cat("third cleanup\n"), add = TRUE)
"value"
}
tryCatch(cleanup(), error = conditionMessage)
#> first cleanup
#> third cleanup
#> [1] "cleanup failed"
# The full order when an error is raised inside a function:
# calling handler -> on.exit -> exiting handler -> finally
failing <- function() {
on.exit(cat("2. on.exit of failing()\n"))
stop("it failed")
}
tryCatch(
withCallingHandlers(
failing(),
error = \(e) cat("1. calling handler (the stack is still there)\n")
),
error = \(e) {
cat("3. exiting handler\n")
"value of the handler"
},
finally = cat("4. finally\n")
)
#> 1. calling handler (the stack is still there)
#> 2. on.exit of failing()
#> 3. exiting handler
#> 4. finally
#> [1] "value of the handler"
# on.exit() belongs to a function; finally belongs to a tryCatch() call.
with_option <- function(value) {
old <- options(digits = value)
on.exit(options(old))
format(pi)
}
with_option(3)
#> [1] "3.14"
format(pi)
#> [1] "3.141593"
Source: R documentation: on.exit
The last example is the commonest use: a function changes a global setting (here options(digits)), restores it in on.exit(), and the caller never sees the change, whether format() succeeds or fails. The two mechanisms are complementary. finally belongs to a tryCatch() call and is the way to clean up after a block that may fail; on.exit() belongs to a function and is the way to make a function leave things as it found them, whoever calls it.
Warnings in detail
When a warning is shown
A warning is a condition, so the handlers come first: a calling handler can log it or muffle it, an exiting handler can stop the evaluation. What happens next depends on options(warn), the integer that ?warning and ?options describe:
warn |
What the default action does |
|---|---|
negative ( |
Warnings are ignored. |
|
Warnings are stored and printed after the top-level function has completed. With 10 or fewer, they are printed; with more, a message says how many there were. The object |
|
Warnings are printed as they occur, as |
|
Warnings are turned into errors. The error message is the warning message with the prefix |
Source: R documentation: options
The manual’s chapter on exception handling states the same rule with a slightly different threshold: it says that with fewer than 10 warnings they are printed after the function has finished and with more than 10 only a count, while ?options says "10 or fewer"; a run with exactly 10 warnings on R 4.6.1 printed all ten, which agrees with the help page. The help page also notes that turning warnings into errors may trigger bugs and resource leaks that would not have happened otherwise, so it is a tool for finding the source of a warning rather than a setting for production code (the debugging page uses it that way). Rscript has no special rule: the default is warn = 0, and the scripts of this page ran with the value set to 1 at start-up so that each warning appears at its own position; the next listing sets 0 explicitly to show the default behaviour, and the output shows that the warnings come after the value of the call.
# The profile this page was run with sets options(warn = 1); this script goes
# back to the default of 0 so that warnings are stored and printed after the
# top-level call has finished.
options(warn = 0)
getOption("warn")
#> [1] 0
overdue_note <- function(member) {
warning("loan overdue for ", member)
member
}
overdue_note("Ana Ruiz")
#> [1] "Ana Ruiz"
#> Warning message:
#> In overdue_note("Ana Ruiz") : loan overdue for Ana Ruiz
cat("next top-level call\n")
#> next top-level call
# Several warnings from one call are printed together, numbered.
audit <- function() {
for (i in 1:3) warning("check digit missing in row ", i)
"audit done"
}
audit()
#> [1] "audit done"
#> Warning messages:
#> 1: In audit() : check digit missing in row 1
#> 2: In audit() : check digit missing in row 2
#> 3: In audit() : check digit missing in row 3
# last.warning is what warnings() prints; the object has class "warnings".
w <- warnings()
class(w)
#> [1] "warnings"
length(w)
#> [1] 3
names(w)
#> [1] "check digit missing in row 1" "check digit missing in row 2"
#> [3] "check digit missing in row 3"
w[[1L]]
#> audit()
# More than ten warnings are not listed, only counted. summary() groups equal
# warnings.
many <- function() {
for (i in 1:12) warning("a row has no ISBN")
"many done"
}
many()
#> [1] "many done"
#> There were 12 warnings (use warnings() to see them)
length(warnings())
#> [1] 12
summary(warnings())
#> 12 identical warnings:
#> In many() : a row has no ISBN
# The limit on stored warnings is options(nwarnings), 50 by default.
options(nwarnings = 5)
many()
#> [1] "many done"
#> Warning messages:
#> 1: In many() : a row has no ISBN
#> 2: In many() : a row has no ISBN
#> 3: In many() : a row has no ISBN
#> 4: In many() : a row has no ISBN
#> 5: In many() : a row has no ISBN
length(warnings())
#> [1] 5
options(nwarnings = 50)
Source: R documentation: warnings
warnings() returns an object of class "warnings", basically a named list: the names are the messages and the elements are the calls. Its length is at most getOption("nwarnings"), 50 by default, which is why the last block (with options(nwarnings = 5)) keeps only five of the twelve. ?warnings suggests raising the limit with something like options(nwarnings = 10000), and ?options adds that the implementation allocates space for the warnings in advance, so a limit above a million or so is a bad idea. summary(warnings()) groups identical warnings and counts them, an addition from R 3.5.0 that the help page calls "somewhat experimental"; the many-warnings case of the listing prints 12 identical warnings:. Before R 4.4.0, warnings() returned NULL when there were no warnings; since then it always inherits from "warnings". The help page also warns that where last.warning is stored is undocumented and subject to change: use warnings(), not the variable.
The other warning options
warn = -1, 1, 2, the immediate. argument, warning.length and warning.expression complete the picture. warning(immediate. = TRUE) turns warn <= 0 into warn = 1 for that call only. warning.length sets the truncation limit of error and warning text in bytes, from 100 to 8170, default 1000; the printed text of a longer warning ends in [... truncated], but the condition object keeps the full text, which the last line of the listing demonstrates (199 characters in the message, a printed line cut at 100). warning.expression is an expression that is called instead of the standard output whenever a warning is generated, whatever the value of warn; the R Language Definition describes it as setting an expression that is evaluated when a warning occurs and says that the normal printing of warnings is suppressed when it is set. showWarnCalls and showErrorCalls control whether the default handlers show a summary of the call stack, which is why script errors have a Calls: line.
# options(warn): negative ignores warnings, 0 stores them, 1 prints them as
# they happen, 2 or more turns them into errors.
overdue_note <- function(member) {
warning("loan overdue for ", member)
member
}
options(warn = -1)
overdue_note("ignored: warn is -1")
#> [1] "ignored: warn is -1"
options(warn = 1)
overdue_note("printed at once: warn is 1")
#> Warning in overdue_note("printed at once: warn is 1") :
#> loan overdue for printed at once: warn is 1
#> [1] "printed at once: warn is 1"
# warn = 2: the warning becomes an error whose message starts with
# "(converted from warning)". It can be caught like any error.
options(warn = 2)
converted <- tryCatch(overdue_note("Ana Ruiz"), error = identity)
converted
#> <simpleError in overdue_note("Ana Ruiz"): (converted from warning) loan overdue for Ana Ruiz>
class(converted)
#> [1] "simpleError" "error" "condition"
# A warning handler sees the warning first, before the conversion.
seen <- tryCatch(
withCallingHandlers(
overdue_note("Ana Ruiz"),
warning = \(w) cat("calling handler got a", class(w)[[1L]], "\n")
),
error = \(e) "then the error"
)
#> calling handler got a simpleWarning
seen
#> [1] "then the error"
options(warn = 1)
# immediate. = TRUE prints this one warning at once even when warn is 0.
options(warn = 0)
urgent <- function() {
warning("printed immediately", immediate. = TRUE)
"urgent done"
}
urgent()
#> Warning in urgent() : printed immediately
#> [1] "urgent done"
options(warn = 1)
# warning.length truncates the text printed by the default handler (100 to
# 8170 bytes, default 1000); the condition object keeps the full text.
options(warning.length = 100)
long_text <- paste(rep("long", 40), collapse = " ")
long_warning <- function() {
warning(long_text)
"long done"
}
long_warning()
#> Warning in long_warning() :
#> long long long long long long long long long long long long long long long long long long long long [... truncated]
#> [1] "long done"
nchar(tryCatch(long_warning(), warning = conditionMessage))
#> [1] 199
options(warning.length = 1000)
# Warnings can be changed into a custom action: warning.expression is called
# instead of the standard output, whatever the value of warn.
options(warning.expression = quote(cat("custom warning hook\n")))
overdue_note("Dev Patel")
#> custom warning hook
#> [1] "Dev Patel"
options(warning.expression = NULL)
Source: R documentation: options
The converted warning in the listing is a simpleError: the original warning is gone, replaced by an error whose message begins with (converted from warning), and a calling handler for warning still sees the warning before the conversion. That is the order to remember when a test suite or a script sets warn = 2 and some code relies on a calling handler for warnings.
Partial matching warnings
Three further options() turn silent behaviour into warnings: warnPartialMatchArgs (partial matching in argument matching), warnPartialMatchAttr (in attr()), and warnPartialMatchDollar (in $). They are explained, with the real warning texts, in Defensive Programming and Input Validation.
Unhandled conditions
What a script does with an error
If no handler takes an error, the default error action runs. The help page of stop() describes it in detail: the message is printed (if options("show.error.messages") is true), the message is stored where geterrmessage() can find it, a trace of the call stack is stored for traceback(), and then the default error handler of options(error = ) applies. With the default NULL value, an interactive session returns to the top level prompt (or the top-level browser), and a non-interactive one, Rscript for instance, "effectively" calls q("no", status = 1, runLast = FALSE): the script ends, R prints Execution halted, and the exit status is 1, which is what a shell script, a scheduler or a CI job look at (see Scripts and the Command Line).
options(catch.script.errors = TRUE) (R 4.4.0, announced in the NEWS as a documented way to catch errors and then continue in non-interactive use) changes that: the error is printed, evaluation jumps to the top level and continues with the next top-level call. It is meant for something like a report generator, and for this page: it lets one script show many different error messages as R prints them. The format of the message is Error in <call> : <message>; when the call and the message do not fit on a line the message goes on the next line, indented; a Calls: line lists the calls that led to the error when there is more than one (showErrorCalls is on by default in non-interactive use); stop(call. = FALSE) prints Error: <message>; and a warning that was recorded before the error (with warn = 0) is reported after it with In addition: Warning message:.
# In a script an unhandled error ends the run with status 1 (R prints
# "Execution halted"). This script sets catch.script.errors = TRUE (R 4.4.0),
# so each error is printed as it would be and the script goes on.
options(catch.script.errors = TRUE, warn = 0)
# "Error in <call> : <message>", and for a nested call a "Calls:" line.
parse_loan <- function(row) check_loan(row)
check_loan <- function(row) stop("loan date is impossible: ", row)
parse_loan("L020")
#> Error in check_loan(row) : loan date is impossible: L020
#> Calls: parse_loan -> check_loan
cat("the script goes on\n")
#> the script goes on
# stop(call. = FALSE) leaves out the call.
stop("no call is shown here", call. = FALSE)
#> Error: no call is shown here
# A long call and message are broken onto a second line.
inventory <- function(place) {
stop("the inventory could not be completed for ", place)
}
inventory("the second floor of the east wing of the main library building")
#> Error in inventory("the second floor of the east wing of the main library building") :
#> the inventory could not be completed for the second floor of the east wing of the main library building
# With warn = 0, warnings that came first are reported as "In addition:".
loan_import <- function(path) {
warning("the loan file is old: ", path)
stop("cannot read ", path)
}
loan_import("loans.csv")
#> Error in loan_import("loans.csv") : cannot read loans.csv
#> In addition: Warning message:
#> In loan_import("loans.csv") : the loan file is old: loans.csv
# Warnings alone: the call is part of the message, or missing for a primitive.
as.integer("x")
#> [1] NA
#> Warning message:
#> NAs introduced by coercion
log(-1)
#> [1] NaN
#> Warning message:
#> In log(-1) : NaNs produced
Source: R documentation: stop
Source: R NEWS: Changes in R 4.4.0
The error option
options(error = ) is the legacy interface for global handling of errors (the help page of conditions calls it that while documenting globalCallingHandlers(), below). It takes a function, called with no arguments, or an expression, and runs after the message has been printed. It is how post-mortem debugging is set up: options(error = recover) shows the frames to browse, and options(error = quote(dump.frames(to.file = TRUE))) saves them, as Debugging shows. It runs before the on.exit() expressions of the functions that are being left, as the R Language Definition says; a quick check on R 4.6.1 with a function whose on.exit() prints a line printed the options(error) line first. Two cautions: in a start-up file such as .Rprofile, the function has to be written with its namespace (options(error = utils::recover)), and the option applies only to errors that no handler caught, not to those caught by tryCatch() or try().
Selective suppression
suppressWarnings(expr, classes = "warning") and suppressMessages(expr, classes = "message") evaluate expr while muffling the conditions that inherit from classes. The classes argument is new in R 4.0.0 and restricts the suppression to conditions that inherit from particular classes. Without it, suppressWarnings() hides every warning of the expression, including the one that explains why a result is wrong, which is the usual way a numeric bug ships; with a class, only the expected warning is hidden, and anything else is reported as before. They are calling handlers that invoke muffleWarning or muffleMessage, nothing more, and the listing shows an equivalent that does by hand what classes = "shelf_odd_year" does.
packageStartupMessage() signals a message of its own class, packageStartupMessage, which suppressPackageStartupMessages() silences separately from other messages (a message from packageStartupMessage() is also a message, so suppressMessages() hides it too). The value of the suppression functions is the value of expr, so they can be wrapped around an assignment. They belong to the call, not to the session: nothing needs to be restored afterwards, unlike with options(warn = -1).
# suppressWarnings() and suppressMessages() evaluate an expression and muffle
# every warning (message) it signals; the value is returned unchanged.
audit <- function() {
warning(warningCondition("year looks odd", class = "shelf_odd_year"))
warning("row 13 has a wrong check digit")
message("audit finished")
"audit value"
}
suppressWarnings(audit())
#> audit finished
#> [1] "audit value"
# classes = limits the suppression to conditions that inherit from them.
# This one is new in R 4.0.0. Other warnings are still reported.
suppressWarnings(audit(), classes = "shelf_odd_year")
#> Warning in audit() : row 13 has a wrong check digit
#> audit finished
#> [1] "audit value"
# The same for messages, with a class of our own (messageCondition: R 4.6.0).
progress <- function() {
message(messageCondition("loading 1/2\n", class = "shelf_progress"))
message("an ordinary message")
"progress value"
}
suppressMessages(progress(), classes = "shelf_progress")
#> an ordinary message
#> [1] "progress value"
suppressMessages(progress())
#> [1] "progress value"
# Suppression is a calling handler that invokes a muffling restart; written
# out by hand, with the warning limited to one class, it looks like this.
by_hand <- withCallingHandlers(
audit(),
shelf_odd_year = \(w) invokeRestart("muffleWarning"),
message = \(m) invokeRestart("muffleMessage")
)
#> Warning in audit() : row 13 has a wrong check digit
by_hand
#> [1] "audit value"
# Package start-up messages have a class of their own.
quiet_start <- function() {
packageStartupMessage("attaching the shelf tools")
"started"
}
suppressPackageStartupMessages(quiet_start())
#> [1] "started"
class(tryCatch(quiet_start(), message = identity))
#> [1] "packageStartupMessage" "simpleMessage" "message"
#> [4] "condition"
Source: R documentation: warning
Source: R NEWS: Changes in R 4.0.0
Global handlers: globalCallingHandlers()
globalCallingHandlers() (R 4.0.0) registers calling handlers for the whole session. They are called only as a last resort, after the handlers registered dynamically with withCallingHandlers() have been invoked, and before the error option. The help page names two uses: a general-purpose logger (for instance saving the last error object in the global workspace) and a general recovery strategy (installing a missing package through the retry_loadNamespace restart). The function takes named handlers like withCallingHandlers() and also an options-like interface: a single list of named handlers establishes them, a single NULL removes all of them and returns the removed list invisibly, and a call with no argument returns the current list. Registering the same handler again moves it to the top of the stack, so that it is called first. Because they are calling handlers, global handlers observe and can muffle (through the restarts) but cannot replace a value.
# globalCallingHandlers() (R 4.0.0) registers calling handlers for the whole
# session, below every handler set up with withCallingHandlers().
globalCallingHandlers()
#> list()
# A logger: it records each condition and returns, so the default action
# (printing the warning or message) still happens.
log_env <- new.env()
log_env$entries <- character()
logger <- function(cnd) {
entry <- paste(class(cnd)[[1L]], conditionMessage(cnd))
log_env$entries <- c(log_env$entries, trimws(entry))
}
globalCallingHandlers(warning = logger, message = logger)
names(globalCallingHandlers())
#> [1] "warning" "message"
message("a message")
#> a message
f <- function() {
warning("a warning")
1
}
f()
#> Warning in f() : a warning
#> [1] 1
log_env$entries
#> [1] "simpleMessage a message" "simpleWarning a warning"
# Registering the same handler again moves it to the top of the stack; NULL
# removes all of them and returns the removed list invisibly.
removed <- globalCallingHandlers(NULL)
names(removed)
#> [1] "warning" "message"
globalCallingHandlers()
#> list()
# A global handler can muffle: here warnings are silenced for the session.
globalCallingHandlers(warning = \(w) {
cat("global handler:", conditionMessage(w), "\n")
invokeRestart("muffleWarning")
})
f()
#> global handler: a warning
#> [1] 1
invisible(globalCallingHandlers(NULL))
# A list of named handlers is accepted too.
globalCallingHandlers(list(message = \(m) cat("seen a message\n")))
message("hello")
#> seen a message
#> hello
invisible(globalCallingHandlers(NULL))
# The logger the help page suggests keeps the last error in a variable. It
# is called for an error that no other handler took, before the error option;
# an error caught by tryCatch() never reaches it.
options(catch.script.errors = TRUE)
globalCallingHandlers(error = \(cnd) log_env$last_error <- cnd)
broken <- function() stop("something broke")
broken()
#> Error in broken() : something broke
class(log_env$last_error)
#> [1] "simpleError" "error" "condition"
log_env$last_error <- NULL
tryCatch(broken(), error = \(e) "caught first")
#> [1] "caught first"
is.null(log_env$last_error)
#> [1] TRUE
invisible(globalCallingHandlers(NULL))
# It must not be called while other handlers are on the stack.
tryCatch(globalCallingHandlers(warning = logger), error = conditionMessage)
#> [1] "should not be called with handlers on the stack"
Source: R documentation: conditions
Two cautions. globalCallingHandlers() must not be called while other handlers are on the stack; the check on R 4.6.1 reports should not be called with handlers on the stack, which is what the last example prints from inside a tryCatch(). R 4.1.3 fixed the behaviour of modifying it inside withCallingHandlers() so that it works or fails correctly. And a global handler that muffles hides the condition from the code that expected to see it, so it belongs in an application’s start-up file or main script and not in a package, which should not change how conditions behave in the session that loads it.
Classes that R uses, and the upcoming objectNotFoundError
Where the language signals errors of its own, a growing number of them carry classes that go beyond simpleError. The NEWS entries behind the classes of R 4.6.1 are: the packageNotFoundError class for many package-not-found errors and deprecatedWarning and defunctError for .Deprecated() and .Defunct() (R 3.6.0); subscriptOutOfBoundsError for out-of-bounds subscripts (R 4.2.0); MissingSubscriptError for c(a = 1, 2)[[]] and the parser’s classed errors (R 4.3.0); and, in R 4.5.0, a classed error for a missing argument, from the missingArgError family. The listing checks each of them on R 4.6.1 and ends with the two errors that have no class yet: "object not found" and "could not find function" are plain simpleError conditions in R 4.6.1, so tryCatch(undefined_object, objectNotFoundError = ...) does not catch anything today and falls through to a generic error handler.
# R signals many of its own errors with more specific classes than
# simpleError. The classes below are the ones R 4.6.1 uses today.
first_class <- function(expr) {
tryCatch(expr, condition = \(cnd) class(cnd)[[1L]])
}
first_class(list(1)[[3]])
#> [1] "subscriptOutOfBoundsError"
first_class(c(a = 1, 2)[[]])
#> [1] "MissingSubscriptError"
first_class(loadNamespace("notAPackage"))
#> [1] "packageNotFoundError"
# A missing argument has a small family of classes (named in the R 4.5.0 NEWS).
tryCatch((function(x) x)(), error = class)
#> [1] "evalError" "missingArgError" "error" "condition"
# .Deprecated() and .Defunct() signal classed conditions (R 3.6.0).
old_api <- function() {
.Deprecated("new_api")
1
}
gone_api <- function() {
.Defunct("new_api")
1
}
first_class(old_api())
#> [1] "deprecatedWarning"
first_class(gone_api())
#> [1] "defunctError"
tryCatch(old_api(), deprecatedWarning = \(w) "caught by class")
#> [1] "caught by class"
# The condition carries fields, for example the package name.
cnd <- tryCatch(loadNamespace("notAPackage"), error = identity)
cnd$package
#> [1] "notAPackage"
conditionMessage(cnd)
#> [1] "there is no package called ‘notAPackage’"
# "Object not found" and "could not find function" are plain simpleError
# conditions in R 4.6.1: there is no class to catch.
first_class(undefined_object)
#> [1] "simpleError"
first_class(undefined_function())
#> [1] "simpleError"
tryCatch(
undefined_object,
objectNotFoundError = \(e) "classed",
error = \(e) "only the generic error handler matched"
)
#> [1] "only the generic error handler matched"
Source: R 3.x NEWS: Changes in R 3.6.0
Source: R documentation: Deprecated
Source: R documentation: Defunct
The development version of R (4.7.0, not released, and subject to change) lists in its NEWS that object-not-found errors now signal conditions of class objectNotFoundError, that function-not-found errors signal the subclass functionNotFoundError, and that the condition classes used internally for some errors are now documented in ?conditions. This is upcoming behaviour that does not exist in R 4.6.1; no example on this page uses it, and the example above is the proof that it is absent. When it arrives, a handler that wants to distinguish a missing object from other errors will be able to name the class instead of matching the text object 'x' not found, as code has to do today. Until then, test the message text only as a last resort, knowing that it depends on the language of the session.
Source: R-devel NEWS: Changes in R-devel
rlang and cli
Two add-on packages extend the conditions system and are common in code written in the tidyverse style; neither is needed to use the mechanisms above, and both build on them (this section used rlang 1.3.0 and cli 3.6.6). rlang::abort() builds a condition and signals it in one step, with the class and the fields as arguments; the class vector it produces is yours followed by rlang_error, error and condition, so base handlers such as tryCatch(shelf_invalid_isbn = ) still work on its conditions. rlang has the matching warn() and inform(), and cnd_signal() for a condition built separately. cli::cli_abort() is a layer on top of abort() that formats the message with cli’s inline markup and bullets (x for what went wrong, i for a hint), and cli_warn() and cli_inform() do the same for the other two kinds. Their documentation is at rlang package documentation: abort and cli package documentation: cli_abort; the formatting of the message is described in rlang package documentation: condition formatting. Since R 3.6.0 base R can build classed conditions without them, and since R 4.6.0 for messages too, so a package that has no other use for rlang does not need it for this.
# rlang 1.3.0 and cli 3.6.6 are add-on packages, not part of base R.
# rlang::abort() builds the condition and signals it; class and fields are
# arguments. The result still matches base handlers.
res <- tryCatch(
rlang::abort(
"invalid ISBN-13: 123",
class = "shelf_invalid_isbn",
isbn = "123"
),
shelf_invalid_isbn = identity
)
class(res)
#> [1] "shelf_invalid_isbn" "rlang_error" "error"
#> [4] "condition"
res$isbn
#> [1] "123"
conditionMessage(res)
#> [1] "invalid ISBN-13: 123"
# cli::cli_abort() formats the message with cli's inline markup and bullets.
limit_error <- tryCatch(
cli::cli_abort(
c(
"Cannot lend {.val {2L}} books to one member.",
"x" = "The limit is {.val {1L}}.",
"i" = "Return a book first."
),
class = "shelf_limit"
),
shelf_limit = identity
)
class(limit_error)
#> [1] "shelf_limit" "rlang_error" "error" "condition"
cat(conditionMessage(limit_error), "\n")
#> Cannot lend 2 books to one member.
#> ✖ The limit is 1.
#> ℹ Return a book first.
Pitfalls collected
The examples above show most of these in passing; the listing gathers the ones that need a demonstration, and the table lists them all in one place.
# 1. next and break do not work inside a handler: the handler is a function,
# and the loop is in the function that called tryCatch().
tryCatch(
for (i in 1:3) {
tryCatch(stop("bad item"), error = \(e) next)
},
error = conditionMessage
)
#> [1] "no loop for break/next, jumping to top level"
# Let the handler return a marker, or put the next in the expression.
for (i in 1:3) {
failed <- tryCatch(
{
if (i == 2) stop("bad item")
cat("item", i, "ok\n")
FALSE
},
error = \(e) TRUE
)
if (failed) next
}
#> item 1 ok
#> item 3 ok
# 2. signalCondition() with an error condition does not stop anything: it
# only looks for handlers. Use stop() to raise an error.
failure <- errorCondition("not raised", class = "shelf_failure")
keeps_going <- function() {
signalCondition(failure)
"keeps_going() continued"
}
keeps_going()
#> [1] "keeps_going() continued"
tryCatch(stop(failure), shelf_failure = \(e) "stop() raised it")
#> [1] "stop() raised it"
# 3. A general handler that always invokes muffleWarning breaks when the
# condition is not a warning.
tryCatch(
withCallingHandlers(
stop("not a warning"),
condition = \(c) invokeRestart("muffleWarning")
),
error = conditionMessage
)
#> [1] "no 'restart' 'muffleWarning' found"
# 4. A handler that swallows every error also swallows the typos.
safe_year <- function(x) tryCatch(as.integer(x) + 1L, error = \(e) NA)
safe_year("2020")
#> [1] 2021
safe_yeer <- function(x) {
tryCatch(as.integer(x) + one, error = \(e) NA) # nolint: object_usage_linter.
}
safe_yeer("2020")
#> [1] NA
Source: R documentation: conditions
| Pitfall | Why it goes wrong | Instead |
|---|---|---|
Matching the message text ( |
The text is meant for people, can be translated ( |
Test the class ( |
|
It swallows every error, including a mistyped variable (the |
Catch the class you expect, keep or log the condition, and let the rest propagate. If a catch-all is needed, record |
A warning handler in |
An exiting handler unwinds: the value of the expression is lost. |
|
|
Only conditions signalled by |
|
|
The handler is a function, not part of the loop: |
Return a marker value and test it after |
|
It only looks for handlers and returns |
|
|
The new condition’s call is |
|
|
The class vector can be longer than one element. |
|
|
It also hides warnings you did not expect. |
|
|
A global switch changes the behaviour of every package; |
Set it for a block and restore it with |
|
The second call replaces the first unless |
|
Calling |
|
Call it at the top level of a script or in a start-up file. |
Relying on the |
It can be the machinery ( |
Store what the program needs in fields; treat the call as a hint for people. |
Source: R documentation: conditions
Version notes
| Version | Change | Source |
|---|---|---|
R 3.1.0 |
|
NEWS 3.1.0 |
R 3.5.0 |
|
NEWS 3.5.0 |
R 3.6.0 |
|
NEWS 3.6.0 |
R 4.0.0 |
|
NEWS 4.0.0 |
R 4.1.3 |
Modifying |
NEWS 4.1.3 |
R 4.2.0 |
Subscript-out-of-bounds errors have the class |
NEWS 4.2.0 |
R 4.3.0 |
|
NEWS 4.3.0 |
R 4.4.0 |
|
NEWS 4.4.0 |
R 4.5.0 |
|
NEWS 4.5.0 |
R 4.6.0 |
|
NEWS 4.6.0 |
R-devel (4.7.0, under development) |
Object-not-found errors get the class |
R-devel NEWS |
The R-devel entries are in the development NEWS; they may change before 4.7.0 is released. The 3.x entries are in the archived NEWS for R 3.x.
Where older tutorials differ
Much that is written about R error handling dates from the time when tryCatch() was the only documented tool, before classed conditions were easy to build, or from the time when try() was the only one. None of it is wrong in the sense of not working, and most of it still runs; the table says what is usually done today and why.
| Older tutorial | Current practice | Why | Version |
|---|---|---|---|
|
|
the class vector may have more than one element, which is an error in an |
R 4.2.0 for the error; the advice is in |
|
|
it keeps the condition, the class and the fields, and |
always |
|
|
the option is global and has to be restored |
always |
Building a custom condition with |
|
the constructors set the class vector correctly and take the extra fields as arguments |
R 3.6.0 and R 4.6.0 |
Telling errors apart by their text, |
A class: your own for your conditions, |
the text is translated and changes |
R 3.6.0 and later |
|
|
only the expected warning is hidden |
R 4.0.0 |
|
|
the exiting handler discards the value |
always |
|
|
a foreign condition may not have the restart |
R 4.0.0 |
|
|
global calling handlers see the condition object, and they can be added and removed |
R 4.0.0 |
|
|
|
R 4.4.0 |
A package is needed to get classed errors with fields ( |
Base R can do it; rlang and cli add formatting, backtraces and |
the base constructors are enough for most packages |
R 3.6.0 |
|
|
the arguments are pasted for you, and translation works |
always |
|
|
the second call replaces the first |
always |
Treating the first |
Fix or classify the warning, |
a hidden warning is a hidden bug |
always |
Source: R documentation: try
For the full list of changes by release see What’s New and Migrating Older Code.
Where to go next
-
Debugging for
traceback(),browser(),options(error = recover)andoptions(warn = 2)as tools to find where a condition was signalled. -
Defensive Programming and Input Validation for
stopifnot(),match.arg(), the partial-matching warnings and failing early with the classed conditions of this page. -
Functions for
on.exit(),sys.call(),match.call()and the call stack that conditions record. -
S3 Classes and Generics for the class vectors and generics (
conditionMessage()is one) behind every condition. -
Lazy Evaluation and Promises for why an error in an argument surfaces where the argument is first used.
-
Scoping, Environments and Closures for
<<-as used by the warning collector. -
Control Flow for loops,
nextandbreakaround atryCatch(). -
The apply Family and Functionals and Function Factories and Higher-Order Functions for wrapping a function so that it catches or retries.
-
Testing with testthat for
expect_error(),expect_warning()andexpect_condition(), which test the classes and messages of this page. -
Scripts and the Command Line for exit status,
Execution haltedandq(status = )in scripts. -
Parallel and Asynchronous Computing for how errors travel from workers to the caller.
-
Idioms and Common Pitfalls for the short list of habits that avoid most of the pitfalls above.
References
-
The R Language Definition: Exception handling (stop, warning, on.exit, Error options)
-
R FAQ 7.32: How can I capture or ignore errors in a long simulation?
-
R NEWS: Changes in R 3.5.0, 4.0.0, 4.1.3, 4.2.0, 4.3.0, 4.4.0, 4.5.0 and 4.6.0; R 3.x NEWS; R-devel NEWS