Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

R (4.6)  Module 4 – Control flow, functions and errors

Errors, warnings and condition handling

Signal errors, warnings and messages in R, recover with tryCatch(), log and go on with withCallingHandlers(), clean up with on.exit(), and catch by class.

  • Intermediate
  • 15 minutes
  • Examples run with R 4.6.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Use stop(), warning() and message() to report errors, problems and progress
  • Recover with tryCatch(), log and continue with withCallingHandlers(), and clean up with on.exit()
  • Create classed conditions and handle them by class while other errors propagate
  • Decide whether to handle, suppress, convert or let a condition propagate

Before you start

What you’ll learn · Before you start

What you will learn

  • Use stop(), warning() and message() to report errors, problems and progress
  • Recover with tryCatch(), log and continue with withCallingHandlers(), and clean up with on.exit()
  • Create classed conditions and handle them by class while other errors propagate
  • Decide whether to handle, suppress, convert or let a condition propagate

Before you start

On this page

as.numeric(c("1200", "1,500", "n/a", "980")) gives 1200 NA NA 980 and one warning, “NAs introduced by coercion”, which says neither how many values failed nor which. Whether a script stops there, logs the two bad values and goes on, or hides the warning altogether is the job of R’s conditions: the errors, warnings and messages that code signals, and the handlers that decide what the code does then.

On runs of R 4.6.1 you will signal each kind with stop(), warning() and message(), recover with tryCatch(), log and carry on with withCallingHandlers(), close files with on.exit() whatever happens, and build errors of your own class that a handler can catch while every other error still stops the program.

stop() ends, warning() complains, message() informs

check_age(-3) stopped with “age must not be negative, not -3”, while mean_positive(c(4, -1, 8)) returned 6 and warned that it had ignored 1 negative value. The three functions differ in what follows them, and that is how to choose between them:

Function What follows Use it for
stop() the call ends, and the error goes up to the caller input the function cannot work with
warning() the function goes on, and R prints the warning a result that is valid but suspect
message() the function goes on, and R prints the text progress and information

The run signals each one and prints the class of the condition: The object R signals to report an error, a warning or a message. It holds a message and the call, and its class vector, such as simpleError, error and condition, is what handlers choose by. it creates:

signals.R, typed at the R console Python · signals_session.py
from session import run, version

version()
# R reads signals.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f signals.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f signals.R
> # stop() ends the function with an error.
> check_age <- function(age) {
+   if (age < 0) stop("age must not be negative, not ", age)
+   age
+ }
> try(check_age(-3))
Error in check_age(-3) : age must not be negative, not -3
> # warning() reports a problem and lets the function finish.
> mean_positive <- function(x) {
+   if (any(x < 0)) warning("ignoring ", sum(x < 0), " negative values")
+   mean(x[x >= 0])
+ }
> mean_positive(c(4, -1, 8))
[1] 6
Warning message:
In mean_positive(c(4, -1, 8)) : ignoring 1 negative values
> # message() reports progress, which is not a problem at all.
> load_rows <- function(n) {
+   message("reading ", n, " rows")
+   seq_len(n)
+ }
> rows <- load_rows(3)
reading 3 rows
> # Each one signals a condition object with a class.
> class(tryCatch(stop("e"), error = function(e) e))
[1] "simpleError" "error"       "condition"  
> class(tryCatch(warning("w"), warning = function(w) w))
[1] "simpleWarning" "warning"       "condition"    
> class(tryCatch(message("m"), message = function(m) m))
[1] "simpleMessage" "message"       "condition"    
> # call. = FALSE leaves the call out of the message.
> check_age2 <- function(age) {
+   if (age < 0) stop("age must not be negative", call. = FALSE)
+   age
+ }
> try(check_age2(-3))
Error : age must not be negative

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 signals_session.py

The help page ?conditions describes the class vectors the run printed: every condition inherits from condition, errors and warnings from error and warning, and stop(), warning() and message() use the classes simpleError, simpleWarning and simpleMessage. Handlers choose by these classes, as the rest of the lesson shows. Two details are visible in the run. The warning came after the result: at the top level, R prints a warning when the call that raised it has finished, with that call in the text, “In mean_positive(c(4, -1, 8))”. And call. = FALSE left the call out of the error, “Error : age must not be negative”, which reads better for users of a function than a call they never typed.

Write the message for the person who will read it

Say what was wrong and what was expected, and include the value: “age must not be negative, not -3” can be fixed without opening the code; “invalid input” cannot.

tryCatch() stops the expression and returns the handler’s value

to_number("12,5") returned NA and a message instead of a bare warning: inside it, tryCatch(as.numeric(text), warning = function(w) ...) caught the warning and returned the handler’s value. The help page ?conditions says that when a condition matches a handler of tryCatch(), control is transferred to the tryCatch() call, the handler is called with the condition, and its result becomes the value of tryCatch(). A handler of this kind is an exiting handler: A handler set up by tryCatch(). When its condition is signalled, R abandons the expression, runs the handler, and returns the handler's value from tryCatch().: the expression is abandoned at the point of the condition. Step through it:

Interactive · message by message How tryCatch() handles a warning, and why the expression stops Step through to_number("12,5"). Messages travel between tryCatch(), the expression it evaluates and the handler it set up.
How tryCatch() handles a warning, and why the expression stops, step 5 of 5. Messages so far: tryCatch() to The expression: as.numeric("12,5"); The expression to tryCatch(): the warning object; tryCatch() to The handler: w, the condition; The handler to tryCatch(): NA_real_.tryCatch()returns NAThe expressionabandonedThe handlerwarning = function(w)as.numeric("12,5")the warning objectw, the conditionNA_real_

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Run the expression. tryCatch() first records which conditions it handles, here warnings, and then evaluates the expression as.numeric("12,5").
  2. A warning is signalled. "12,5" is not a number in R, so as.numeric() signals the warning "NAs introduced by coercion".
  3. The expression is abandoned. tryCatch() has a handler for warnings, so R leaves the expression at once. Whatever came after as.numeric() would never run.
  4. The handler runs. tryCatch() calls the handler with the condition. It prints the message with conditionMessage(w) and returns NA_real_.
  5. The handler's value is returned. The value of the handler becomes the value of tryCatch(), so to_number("12,5") returns NA. A finally expression would run now, last of all.

The run shows the handler’s value, the abandoned expression, finally and the call that signalled:

trycatch.R, typed at the R console Python · trycatch_session.py
from session import run, version

version()
# R reads trycatch.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f trycatch.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f trycatch.R
> # tryCatch() returns the handler's value when a condition it handles is signalled.
> to_number <- function(text) {
+   tryCatch(
+     as.numeric(text),
+     warning = function(w) {
+       message("could not read '", text, "': ", conditionMessage(w))
+       NA_real_
+     }
+   )
+ }
> to_number("12.5")
[1] 12.5
> to_number("12,5")
could not read '12,5': NAs introduced by coercion
[1] NA
> # The expression stops at the condition: nothing after it runs.
> tryCatch({
+   x <- as.numeric("abc")
+   cat("this line never runs\n")
+   x
+ }, warning = function(w) "stopped at the warning")
[1] "stopped at the warning"
> # finally runs whatever happens.
> tryCatch(stop("boom"), error = function(e) conditionMessage(e), finally = cat("finally ran\n"))
finally ran
[1] "boom"
> # conditionCall() gives the call that signalled it.
> tryCatch(log(-1), warning = function(w) deparse(conditionCall(w)))
[1] "log(-1)"
> # try() is the short form for errors: it returns an object of class "try-error".
> res <- try(log("ten"), silent = TRUE)
> class(res)
[1] "try-error"
> inherits(res, "try-error")
[1] TRUE

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trycatch_session.py

The line after as.numeric("abc") never ran: tryCatch() left the expression at the warning, so its result was the handler’s “stopped at the warning”. finally runs last whether or not a condition happened, which suits clean-up that belongs to the caller. conditionMessage(e) and conditionCall(w) read the parts of a condition, here "boom" and "log(-1)". try(expr, silent = TRUE) is the short form for errors: it returns the value, or an object of class "try-error" holding the message. Test it with inherits(res, "try-error"), which looks through the whole class vector, rather than comparing class(res) with ==.

Catching warnings with tryCatch() when the result matters

tryCatch(expr, warning = ...) stops expr at its first warning, so a long computation that warns once returns only the handler’s value and none of its work. To note a warning and keep the result, use withCallingHandlers(), the next section.

Checkpoint

Check: Errors, warnings and messages · Recovering with tryCatch() and try()

  1. Question 1 of 2 A function receives text where it needs numbers and cannot compute anything useful. What should it signal?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: An error, with stop()

    check_age(-3) in the run stopped with a message that named the bad value.

  2. Question 2 of 2 What does this return?

    Read the code, then choose one answer.

    tryCatch({
      x <- as.numeric("abc")
      "after the conversion"
    }, warning = function(w) "handled")
    Show the answer: Question 2 of 2

    Answer: "handled"

    The run's second tryCatch() printed "stopped at the warning" for the same reason.

withCallingHandlers() handles a condition and carries on

withCallingHandlers(as.numeric(amounts), warning = ...) printed “logged: NAs introduced by coercion” and still returned all four values, 1200 NA NA 980. A calling handler: A handler set up by withCallingHandlers(). It runs where the condition was signalled, and the code carries on afterwards unless the handler leaves or invokes a restart. runs at the point where the condition was signalled, without leaving the expression, as ?conditions describes: if the handler returns, R tries the next handler and then carries on. Inside it, invokeRestart("muffleWarning") uses a restart: A point that code sets up for a handler to jump back to. warning() provides muffleWarning and message() muffleMessage, so a calling handler can silence one and let the code go on. that warning() provides, so the warning counts as dealt with and is not printed. Compare the path with the previous figure:

Interactive · message by message How withCallingHandlers() handles a warning and carries on Step through the conversion of the four amounts. Unlike tryCatch(), the handler runs inside the expression, which then goes on.
How withCallingHandlers() handles a warning and carries on, step 5 of 5. Messages so far: withCallingHandlers() to The expression: as.numeric(amounts); The expression to The handler: w, the condition; The handler to The expression: muffleWarning restart; The expression to withCallingHandlers(): 1200 NA NA 980.withCallingHandlers()returns the numbersThe expressioncontinuesThe handlerwarning = function(w)as.numeric(amounts)w, the conditionmuffleWarning restart1200 NA NA 980

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. Run the expression. withCallingHandlers() records its warning handler and evaluates as.numeric(amounts) on "1200", "1,500", "n/a" and "980".
  2. A warning, mid-way. Two of the four cannot be read, so as.numeric() signals "NAs introduced by coercion" while it is still running.
  3. The handler runs in place. R calls the handler at once, from inside the expression, without leaving it. The handler logs the message.
  4. Muffle and return. invokeRestart("muffleWarning") tells R that the warning has been dealt with, so it is not printed, and control goes back to where it was signalled.
  5. The expression finishes. as.numeric() completes and returns 1200 NA NA 980, which becomes the value of withCallingHandlers(). Nothing was abandoned.

The run logs the warning of the four amounts, then uses the same pattern in a reader that converts every column of a data frame:

calling_handlers.R, typed at the R console Python · calling_handlers_session.py
from session import run, version

version()
# R reads calling_handlers.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f calling_handlers.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f calling_handlers.R
> # withCallingHandlers() runs the handler where the warning happens, then carries on.
> amounts <- c("1200", "1,500", "n/a", "980")
> withCallingHandlers(
+   as.numeric(amounts),
+   warning = function(w) {
+     message("logged: ", conditionMessage(w))
+     invokeRestart("muffleWarning")
+   }
+ )
logged: NAs introduced by coercion
[1] 1200   NA   NA  980
> # A reader that converts every column, records what failed and prints no warning.
> read_numbers <- function(df) {
+   failed <- list()
+   warnings_muffled <- 0
+   for (col in names(df)) {
+     text <- df[[col]]
+     values <- withCallingHandlers(
+       as.numeric(text),
+       warning = function(w) {
+         warnings_muffled <<- warnings_muffled + 1
+         invokeRestart("muffleWarning")
+       }
+     )
+     failed[[col]] <- text[is.na(values) & !is.na(text)]
+     df[[col]] <- values
+   }
+   list(data = df, failed = failed, warnings = warnings_muffled)
+ }
> raw <- data.frame(amount = c("1200", "1,500", "980"), units = c("3", "two", "5"))
> result <- read_numbers(raw)
> result$data
  amount units
1   1200     3
2     NA    NA
3    980     5
> result$failed
$amount
[1] "1,500"

$units
[1] "two"

> result$warnings
[1] 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 calling_handlers_session.py

read_numbers() converted both columns, kept every value that could be read, and returned the text that failed, "1,500" in amount and "two" in units, with the number of warnings it muffled, 2. Nothing was printed while it ran, yet nothing was lost: the log says exactly what to fix in the data. Note how the handler counts: it changes warnings_muffled with <<-, which finds the variable in read_numbers()’s environment, the closure rule of the lesson on scoping. The help page recommends tryInvokeRestart("muffleWarning") when the warning may come from code you do not control, because it returns quietly if that restart is missing.

Key idea

tryCatch() abandons the expression and returns the handler’s value; withCallingHandlers() runs the handler where the condition happened and lets the expression finish, so it is the one to use for logging and counting.

Checkpoint

Check: Logging with withCallingHandlers()

  1. Question 1 of 2 What does this return?

    Read the code, then choose one answer.

    withCallingHandlers({
      warning("w")
      "done"
    }, warning = function(w) invokeRestart("muffleWarning"))
    Show the answer: Question 1 of 2

    Answer: "done", with nothing printed

    A calling handler runs where the condition happened; muffling lets the code go on as if nothing was signalled.

  2. Question 2 of 2 What does invokeRestart("muffleWarning") do inside a calling handler for a warning?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It marks the warning as dealt with, so it is not printed, and the code carries on

    The run's reader muffled 2 warnings and returned every column.

on.exit() cleans up however a function ends

first_line() printed “connection closed” both when it returned “id,amount” and when it stopped with “the first line is empty”. The help page ?on.exit says the expression it records is run when the current function exits, either naturally or as the result of an error. So the clean-up is registered right after the resource is opened, before anything that can fail. Step through both calls:

Interactive · stage by stage on.exit() runs on every way out of a function Step through first_line() on the two files of the run. The column is a normal call; the branch is the path an error takes, which reaches the clean-up all the same.
on.exit() runs on every way out of a function, step 6 of 6. Showing con <- file(path, "r"), on.exit(close(con)), readLines(con, n = 1), the last value: line (never reached), on.exit code runs, The caller gets it (gets the error). In focus: The caller gets it.stop(): an errorcon <- file(path, "r")a connection is openon.exit(close(con))registered, not run yetreadLines(con, n = 1)the first linethe last value: linenever reachedon.exit code runsclose(con), then the messageThe caller gets itgets the error

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Open and register. The function opens a connection and at once registers its clean-up with on.exit(). Nothing is closed yet; the code is kept for the moment the function ends.
  2. A normal call. For the file with a header, the first line is "id,amount", so the function reaches its last value without an error.
  3. The clean-up runs. As the function ends, R runs the on.exit code, which closes the connection and prints "connection closed", before the caller receives "id,amount".
  4. An empty first line. For the second file the first line is "", so stop("the first line is empty") is called while the connection is still open.
  5. The error path cleans up too. The error leaves the function at once, but R still runs the on.exit code on the way out, so "connection closed" printed before the error message.
  6. The error goes on. The clean-up does not swallow the error. try() in the caller receives it and prints "the first line is empty"; the connection is closed either way.

The run reads two files, then shows how several on.exit() calls combine:

on_exit.R, typed at the R console Python · on_exit_session.py
from session import run, version

version()
# R reads on_exit.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f on_exit.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f on_exit.R
> # on.exit() runs when the function ends, normally or with an error.
> first_line <- function(path) {
+   con <- file(path, "r")
+   on.exit({
+     close(con)
+     cat("connection closed\n")
+   }, add = TRUE)
+   line <- readLines(con, n = 1)
+   if (line == "") stop("the first line is empty")
+   line
+ }
> path <- tempfile(fileext = ".csv")
> writeLines(c("id,amount", "1,1200"), path)
> first_line(path)
connection closed
[1] "id,amount"
> writeLines(c("", "1,1200"), path)
> try(first_line(path))
connection closed
Error in first_line(path) : the first line is empty
> # Without add = TRUE, a second on.exit() replaces the first.
> replaced <- function() {
+   on.exit(cat("first cleanup\n"))
+   on.exit(cat("second cleanup\n"))
+   "done"
+ }
> replaced()
second cleanup
[1] "done"
> both <- function() {
+   on.exit(cat("first cleanup\n"))
+   on.exit(cat("second cleanup\n"), add = TRUE)
+   "done"
+ }
> both()
first cleanup
second cleanup
[1] "done"
> # after = FALSE puts a cleanup in front of the others.
> in_front <- function() {
+   on.exit(cat("added first\n"))
+   on.exit(cat("added second, runs first\n"), add = TRUE, after = FALSE)
+   invisible()
+ }
> in_front()
added second, runs first
added first
> unlink(path)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 on_exit_session.py

Without add = TRUE, the second on.exit() replaced the first, so replaced() printed only “second cleanup”. With add = TRUE both ran, in the order they were added, and after = FALSE put a later one in front. Write on.exit(..., add = TRUE) every time: then a clean-up added later, in the same function or by an edit next year, cannot silently cancel one that is already there.

Closing a connection at the end of the function

close(con) on the last line runs only when nothing before it fails. Register it right after file() with on.exit(close(con), add = TRUE), so an error between the two cannot leave the connection open.

Checkpoint

Check: Clean-up with on.exit()

  1. Question 1 of 2 What does try(h()) print?

    Read the code, then choose one answer.

    h <- function() {
      on.exit(cat("closed\n"))
      stop("fail")
    }
    Show the answer: Question 1 of 2

    Answer: "closed", then the error "fail"

    first_line() in the run printed "connection closed" before "the first line is empty".

  2. Question 2 of 2 A function calls on.exit(close(a)) and later on.exit(close(b)). What runs when it ends?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only close(b)

    Write on.exit(..., add = TRUE) every time.

Classed conditions let a handler choose what it catches

validate_age(c(34, -2, 51, 230)) stopped with an error of class invalid_age that carried the bad rows, 2 and 4, and their values, -2 and 230. errorCondition(message, class = ..., ...), in base R since R 3.6.0, builds such an error: the class goes in front of error and condition, and every extra argument becomes a field. A tryCatch() handler named after the class catches that error and no other, so an unexpected failure still stops the program instead of being mistaken for bad ages. Step through two errors meeting the same handler:

Interactive · step by step A handler catches only the classes it names Step through tryCatch(..., invalid_age = handler) with two different errors. Each error carries a class vector; the handler is chosen by matching one of those classes.
A handler catches only the classes it names, step 4 of 4. Showing validate_age(ages), log("ten"), invalid_age handler, Goes on to the caller. In focus: Goes on to the caller.class matchesno handler for itvalidate_age(ages)invalid_age, error, conditionlog("ten")simpleError, error, conditioninvalid_age handlerfunction(e) eGoes on to the callertry() prints it

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. A classed error. validate_age() stops with errorCondition(..., class = "invalid_age"), so the condition's class is c("invalid_age", "error", "condition"), and it carries rows and values.
  2. The class matches. tryCatch() has a handler named invalid_age, one of the condition's classes, so that handler runs. It returns the condition itself, with rows 2 4 and values -2 230.
  3. An unrelated error. log("ten") stops with a simpleError, whose classes are simpleError, error and condition. None of them is invalid_age.
  4. It passes through. With no handler for its classes, the error leaves tryCatch() as if it were not there, and try() around it prints "non-numeric argument to mathematical function". Unexpected errors still stop the program.

The run catches the classed error, lets an unrelated one through, and handles a classed message:

custom_conditions.R, typed at the R console Python · custom_conditions_session.py
from session import run, version

version()
# R reads custom_conditions.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f custom_conditions.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f custom_conditions.R
> # errorCondition() builds an error with a class and data of your own.
> validate_age <- function(age) {
+   bad <- which(age < 0 | age > 120)
+   if (length(bad) > 0) {
+     stop(errorCondition(
+       paste("invalid age in rows", paste(bad, collapse = ", ")),
+       class = "invalid_age", rows = bad, values = age[bad]
+     ))
+   }
+   age
+ }
> ages <- c(34, -2, 51, 230)
> caught <- tryCatch(validate_age(ages), invalid_age = function(e) e)
> class(caught)
[1] "invalid_age" "error"       "condition"  
> conditionMessage(caught)
[1] "invalid age in rows 2, 4"
> caught$rows
[1] 2 4
> caught$values
[1]  -2 230
> # A handler for one class lets every other error through.
> try(tryCatch(log("ten"), invalid_age = function(e) "never reached"))
Error in log("ten") : non-numeric argument to mathematical function
> # messageCondition() (R 4.6.0) builds a classed message.
> note <- messageCondition("3 rows dropped", class = "dropped_rows")
> class(note)
[1] "dropped_rows" "message"      "condition"   
> withCallingHandlers(
+   {
+     message(note)
+     "finished"
+   },
+   dropped_rows = function(m) {
+     cat("handled:", conditionMessage(m), "\n")
+     invokeRestart("muffleMessage")
+   }
+ )
handled: 3 rows dropped 
[1] "finished"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 custom_conditions_session.py

The handler received the whole condition, so the caller could read caught$rows and caught$values instead of parsing the message. log("ten") raised a simpleError, which matched no handler and reached try() unchanged. R 4.6.0 added messageCondition(), which does the same for messages: the handler for dropped_rows received “3 rows dropped” and muffled it with invokeRestart("muffleMessage").

Which treatment a condition deserves depends on two questions: whether the computation should go on, and whether you have already decided what the condition means for your data. Answer them for a condition in your own code:

Interactive · answer and compare What should your code do with this condition? Answer two questions about the condition your code meets. The table marks the ways that fit and says why the others do not.
After the condition, should the computation go on?
Have you already decided what the condition means for the data?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Five ways to deal with a condition in R, against four things the code may need afterwards
Way Keeps the full result Leaves a record Other errors still stop No noise for users
Let it propagate What happens: the caller sees it Mixed a warning keeps the result; an error ends the call Good the message reaches the caller and the log Good nothing is caught Poor every warning is printed
tryCatch() What happens: stops the expression, returns the handler's value Poor the expression stops at the condition: to_number() gave NA Mixed only what the handler writes down Good a handler by class lets other errors through Good the handler decides what to show
withCallingHandlers() What happens: runs the handler, then carries on Good as.numeric() finished: 1200 NA NA 980 Good the handler can count or log each one Good only the named class is handled Good muffleWarning hides it after logging
suppressWarnings() What happens: the warning disappears Good the computation finishes Poor no trace of what went wrong Good errors are not affected Good nothing printed
options(warn = 2) What happens: every warning becomes an error Poor the first warning ends the computation Good the warning becomes an error with its message Good errors stay errors Poor every warning stops the run
How the answers decide
  • “After the condition, should the computation go on?” Yes, finish it: rules out tryCatch() and options(warn = 2). tryCatch() abandons the expression at the condition, and warn = 2 turns the first warning into an error; neither lets the computation finish.
  • “After the condition, should the computation go on?” No, stop or give up: rules out suppressWarnings() and adds a caveat to withCallingHandlers(). Silencing hides the reason for stopping; a calling handler can log, but something else must then stop the run.
  • “Have you already decided what the condition means for the data?” No, I need to see it: rules out suppressWarnings(). suppressWarnings() is fine only when you already know what the NAs mean; it leaves no record to decide from.
  • “Have you already decided what the condition means for the data?” Yes, and I can say it: adds a caveat to Let it propagate. Once the meaning is decided, repeating the warning to every user adds noise; handle it and record the count instead.

In short: let errors you cannot handle propagate; catch the classes you can handle with tryCatch(); log and count with withCallingHandlers(); use suppressWarnings() only after you have decided what the warning means; and set options(warn = 2) in tests, where the help page ?options says warnings are turned into errors.

Checkpoint

Check: Classed conditions and choosing a treatment

  1. Question 1 of 2 With validate_age() as in the run, what does this return?

    Read the code, then choose one answer.

    tryCatch(validate_age(c(30, 500)), invalid_age = function(e) e$values)
    Show the answer: Question 1 of 2

    Answer: 500

    Fields given to errorCondition() travel with the condition to the handler.

  2. Question 2 of 2 When is suppressWarnings(as.numeric(x)) the right call?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: When you have already decided what the NAs mean and record how many there are

    Without a record, suppressing hides data problems; with withCallingHandlers() you can count and muffle at once.

Practise: predict, then check

Give each line an answer before you read the run: what it returns, and what it prints on the way.

Your predictions

Predict tryCatch(stop("a"), error = function(e) "b"), a tryCatch() of a warning with a finally that prints “done”, a withCallingHandlers() that muffles a warning in a block whose last value is “kept going”, inherits(try(sqrt("4"), silent = TRUE), "try-error"), and a function whose on.exit() prints “bye” before it returns “hi”. Then type the lines into R.

practice.R, typed at the R console Python · practice_session.py
from session import run, version

version()
# R reads practice.R as if each line were typed at the console.
run("""
R --vanilla --quiet -f practice.R
""")

Output

# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f practice.R
> tryCatch(stop("a"), error = function(e) "b")
[1] "b"
> tryCatch(warning("w"), warning = function(w) 1, finally = cat("done\n"))
done
[1] 1
> withCallingHandlers({ warning("w"); "kept going" }, warning = function(w) invokeRestart("muffleWarning"))
[1] "kept going"
> r <- try(sqrt("4"), silent = TRUE)
> inherits(r, "try-error")
[1] TRUE
> f <- function() { on.exit(cat("bye\n")); "hi" }
> f()
bye
[1] "hi"

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

The error handler’s value replaced the error; finally printed “done” before the handler’s 1 was returned; the calling handler muffled the warning and the block went on to “kept going”; sqrt() of text is an error, so try() returned a "try-error"; and the clean-up printed “bye” before the function’s value came back.

Then the adaptive questions, your least practised concept first, each with the reason for every choice:

Practice that adapts to you

Practise this lesson

16 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 At the console, when does R print a warning raised inside a function?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: After the top-level call that raised it has finished

    ?options describes warn: 0 stores warnings until the top-level function returns, 1 prints them as they occur, 2 turns them into errors.

  2. Question 2 of 16 Why report progress with message() rather than cat()?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: message() signals a condition that callers can muffle, for example with suppressMessages()

    The run's load_rows() printed "reading 3 rows" through message(), and a handler for class message could catch it.

  3. Question 3 of 16 What does stop("age must not be negative", call. = FALSE) change?

    Choose one answer.

    Show the answer: Question 3 of 16

    Answer: The message no longer starts with the call, so it reads "Error : age must not be negative"

    Leaving the call out helps users who never typed that call; keep it in code where the call helps debugging.

  4. Question 4 of 16 When does the finally expression of tryCatch() run?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: Every time, after the expression or the handler has finished

    ?conditions: the finally expression is evaluated in the context where tryCatch() was called, with its handlers no longer active.

  5. Question 5 of 16 res <- try(f(), silent = TRUE). Which test of whether f() failed is the right one?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: inherits(res, "try-error")

    The run printed TRUE for inherits(res, "try-error") after try(log("ten")).

  6. Question 6 of 16 g <- function(x) tryCatch(log(x), warning = function(w) 0). What is g(-1) + g(exp(2))?

    Type a number.

    A hint

    log(-1) warns; log(exp(2)) does not.

    Show the answer: Question 6 of 16

    Answer: 2

    log(-1) signals a warning, so g(-1) returns the handler's 0; log(exp(2)) is 2 with no condition, so the total is 2.

  7. Question 7 of 16 Which construct lets an expression finish after its handler has run?

    Choose one answer.

    Show the answer: Question 7 of 16

    Answer: withCallingHandlers()

    The run's withCallingHandlers() logged the warning and still returned all four numbers.

  8. Question 8 of 16 How many warnings does as.numeric(c("x", "y", "z")) signal?

    Type a number.

    A hint

    Count the calls that warn, not the values that fail.

    Show the answer: Question 8 of 16

    Answer: 1 warnings

    as.numeric() warns once per call, whatever the number of failures, which is why the reader of the run recorded the failed text itself and not only the warnings.

  9. Question 9 of 16 A long simulation warns now and then. You need every warning in a log file and every result kept. Which design fits?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: Wrap it in withCallingHandlers() with a handler that writes the warning to the log and muffles it

    Calling handlers observe; exiting handlers recover.

  10. Question 10 of 16 Where should on.exit(close(con), add = TRUE) go?

    Choose one answer.

    Show the answer: Question 10 of 16

    Answer: Right after the line that opens con

    Register the clean-up as soon as there is something to clean up.

  11. Question 11 of 16 What does k() print?

    Read the code, then choose one answer.

    k <- function() {
      on.exit(cat("A\n"))
      on.exit(cat("B\n"), add = TRUE, after = FALSE)
      invisible()
    }
    Show the answer: Question 11 of 16

    Answer: B, then A

    in_front() in the run printed "added second, runs first" before "added first".

  12. Question 12 of 16 What does f() return?

    Read the code, then choose one answer.

    f <- function() {
      x <- 1
      on.exit(x <- 2)
      x
    }
    Show the answer: Question 12 of 16

    Answer: 1

    on.exit() is for clean-up, not for changing the result.

  13. Question 13 of 16 A handler receives e, the invalid_age error of the run. How does it read the bad rows?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: e$rows

    Data in fields beats data in message text: the caller gets it without parsing.

  14. Question 14 of 16 What is class(errorCondition("m", class = "invalid_age"))?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: "invalid_age" "error" "condition"

    The run printed this class vector for the caught condition.

  15. Question 15 of 16 tryCatch(log("ten"), invalid_age = function(e) "never reached"). What happens?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: The error from log() is not handled and goes on to the caller

    Catching by class lets unexpected errors stop the program, which is the point.

  16. Question 16 of 16 Which handler runs?

    Read the code, then choose one answer.

    tryCatch(
      stop(errorCondition("bad", class = "my_error")),
      error = function(e) "error handler",
      my_error = function(e) "my handler"
    )
    Show the answer: Question 16 of 16

    Answer: "error handler"

    List specific classes before general ones, my_error before error.

Interview questions

Warm-up: what is the difference between tryCatch() and withCallingHandlers()?

Both establish handlers for classes of conditions, but they do different things when one is signalled. A handler of tryCatch() is an exiting handler: R abandons the expression at the point of the condition, calls the handler, and the handler’s value becomes the value of tryCatch(), so any work after the condition is lost. A handler of withCallingHandlers() is a calling handler: it runs where the condition was signalled, and when it returns, the expression carries on, so a warning can be logged, counted or muffled with invokeRestart("muffleWarning") while the computation finishes. Use tryCatch() to recover from an error with a fallback value, and withCallingHandlers() to observe or record conditions without changing the result.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A nightly job processes 300 files. One malformed file stops the whole run, and an earlier fix that wrapped everything in tryCatch() made failures silent. Design the error handling so good files are processed, bad ones are reported with their reason, and unexpected errors still stop the job.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A function opens a database connection and writes a temporary file. When it fails halfway, connections pile up until the database refuses new ones, and temporary files stay behind. How do you make the clean-up reliable, and how do you test that it runs when the function fails?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • stop() ends the call with an error, warning() lets it finish and prints the problem, and message() reports progress; each signals a condition whose class vector handlers use to choose.
  • tryCatch() abandons the expression at a matching condition and returns the handler’s value, with finally run last; try() returns an object of class "try-error", tested with inherits().
  • withCallingHandlers() runs its handler where the condition happened and lets the expression finish, so it can log or count warnings and muffle them with invokeRestart("muffleWarning").
  • on.exit(..., add = TRUE) registers clean-up that runs however the function ends, errors included; without add = TRUE, a later on.exit() replaces it.
  • errorCondition() and messageCondition() build conditions of your own class with data attached, so a handler can catch exactly them while other errors still propagate.

Terms in this lesson

calling handler
A handler set up by withCallingHandlers(). It runs where the condition was signalled, and the code carries on afterwards unless the handler leaves or invokes a restart.
condition
The object R signals to report an error, a warning or a message. It holds a message and the call, and its class vector, such as simpleError, error and condition, is what handlers choose by.
exiting handler
A handler set up by tryCatch(). When its condition is signalled, R abandons the expression, runs the handler, and returns the handler's value from tryCatch().
restart
A point that code sets up for a handler to jump back to. warning() provides muffleWarning and message() muffleMessage, so a calling handler can silence one and let the code go on.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.