Applying R to Lifestyle and Brain Health Research
University of Kansas Medical Center
September 23, 2026
Provides a paired set of tools that allows the author of a function to indicate that something unusual is happening and a user to deal with it. The author signals conditions with functions like stop() (for errors), warning() (for warnings), and message() (for messages).
The user can handle the signals with functions like tryCatch() and withCallingHandlers().
Conditions are displayed prominently with errors always starting with Error and warnings starting with Warning.
Errors are signalled, or thrown using stop() Adding call. = FALSE removes the function call which is typically not useful and can be obtained from traceback. This can be accomplished automatically with rlang::abort.
The best error messages tell you what is wrong and provides direction toward fixing the problem. Writing good error messages is challenging because it is hard to imagine how the user is thinking incorrectly about your code.
You can visit the tidyverse style guide for helpful tips in writing error messages.
Warnings signal that something has gone wrong, but the code recovered and was able to continue. You can have multiple warnings within a function.
Warnings are cached and printed only when a function exists, but you can control this behavior with the warn option.
options(warn = 0) is the default behavior and prints warnings at the end.options(warn = 1) will make the warnings appear immediately.options(warn = 2) will turn the warnings into errors (helps with debugging a warning).It is difficult to know when to use warnings. Warnings are often missed if there is a lot of other output and restraining use of warnings is advised. Warnings are appropriate when:
Provide information to let the user know you have done something on their behalf. Good messages provide useful information so the user knows what is happening, but not too many details that the user feels overwhelmed. Messages are recommended when:
.onAttach())The results of the message() and cat() functions are similar but their purposes are different. Use cat when the user asks for a message to be printed to the console and message when the developer elects for a message to be printed.
A simple way to handle conditions in R is to ignore them. This can be done using try for errors, suppressWarnings for warnings, and suppressMessages for messages.
The try() function allows execution to continue after an error has occurred.
By doing assignment within the call, you can define a default value for use if the code does not work. This works because the argument is evaluated in the calling environment and not the function.
Both suppressWarnings() and suppressMessages() suppress all warnings and messages.
Each signalling condition has a default behavior:
Condition handlers allow us to temporarily override or supplement the default behavior. We can use tryCatch and withCallingHandlers to register handlers.
tryCatch(
error = function(condition) {
# code to run when an error is thrown
},
# code to run while handlers are active
)
withCallingHandlers(
warning = function(condition) {
# code to run when a warning is signalled
},
message = function(condition) {
# code to run when a message is signalled
},
# code to run while handlers are active
)Viewing a condition object that is signalled can be done with rlang::catch_cnd. Built-in conditions have two elements:
The function tryCatch can be used to handle error conditions. You can override the default error behavior (e.g., return NA instead of an error) and code executes normally if no error exists.
It is called an exiting handler because control passes to the handler after the error occurs and never returns to the original code.
The handler functions are called with a single argument (i.e., the condition object). This object becomes more useful when you make your own custom conditions.
The tryCatch function also has a finally argument to provide a block of code that runs regardless of whether the initial expression is successful. This could be useful for clean up (e.g., deleting files) or closing connections.
Unlike exiting handlers, calling handlers like withCallingHandlers allow code execution to continue normally once the handler returns, making a more natural pairing with the non-error conditions (i.e., message, warning). Notice how the execution of the code below differs:
tryCatch(
message = function(cnd) cat("Here is a message!\n"),
{
message("Is someone available?")
message("Why, yes! How can I help?")
}
)
#> Here is a message!
withCallingHandlers(
message = function(cnd) cat("Here is a message!\n"),
{
message("Is someone available?")
message("Why, yes! How can I help?")
}
)
#> Here is a message!
#> Is someone available?
#> Here is a message!
#> Why, yes! How can I help?Handlers are applied in order so you do not need to worry about infinite loops, but you may want to think through the order if using multiple handlers.
A unique side-effect of calling handlers is the ability to muffle the signal. A condition will continue to propagate to the parent handlers all the way to the parent or exiting handler.
# Bubbles up to the default handler which generates the message
withCallingHandlers(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Greetings!")
)
)
#> Level 1
#> Level 2
#> Greetings!
# Muffles the default handler
withCallingHandlers(
message = function(cnd) {
cat("Level 2\n")
rlang::cnd_muffle(cnd)
},
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Greetings!")
)
)
#> Level 1
#> Level 2R has the ability to create custom conditions containing additional metadata. Using rlang::abort allows you to apply a custom .subclass and additional metadata, guiding the user toward a solution to the problem.
my_log <- function(x, base = exp(1)) {
if (!is.numeric(x)) {
rlang::abort(
paste0(
"`x` must be a numeric vector; not ",
typeof(x),
"."
)
)
}
if (!is.numeric(base)) {
rlang::abort(
paste0(
"`base` must be a numeric vector; not ",
typeof(base),
"."
)
)
}
base::log(x, base = base)
}
my_log(letters)
#> Error in `my_log()`: `x` must be a numeric vector; not character.
my_log(1:10, base = letters)
#> Error in `my_log()`: `base` must be a numeric vector; not character.Let’s improve our my_log error handling by making it more generalized, including a nice error message with the glue R package for the user and more metadata for the developer.
# An example of doing this in Base R is in Advaned R on page 191
abort_bad_argument <- function(arg, must, not = NULL) {
msg <- glue::glue("`{arg}` must {must}")
if (!is.null(not)) {
not <- typeof(not)
msg <- glue::glue("{msg}; not {not}")
}
rlang::abort(
"error_bad_argument",
message = msg,
arg = arg,
must = must,
not = not
)
}
my_log <- function(x, base = exp(1)) {
if (!is.numeric(x)) {
abort_bad_argument("x", must = "be numeric", not = x)
}
if (!is.numeric(base)) {
abort_bad_argument("base", must = "be numeric", not = base)
}
base::log(x, base = base)
}
my_log(letters)
#> Error in `abort_bad_argument()`: ! `x` must be numeric; not character
my_log(1:10, base = letters)
#> Error in `abort_bad_argument()`: ! `base` must be numeric; not characterThis structure makes programming more efficient and can be used in a wide range of scenarios including unit testing in package development.
Some common applications of the condition system in R:
Return a default value if an error occurs
A more sophisticated application is base::try():
try2 <- function(expr, silent = FALSE) {
tryCatch(
error = function(cnd) {
msg <- conditionMessage(cnd)
if (!silent) {
message("Error: ", msg)
}
structure(msg, class = "try-error")
},
expr
)
}
try2(1)
#> [1] 1
try2(stop("Hello!"))
#> Error: Hello!
#> [1] "Hello!"
#> attr(,"class")
#> [1] "try-error"
try2(stop("Hello!"), silent = TRUE)
#> [1] "Hello!"
#> attr(,"class")
#> [1] "try-error"An extension of the failure value by returning a different value if the code evaluates successfully. This requires an evaluation of the code supplied by the user.
We can use this to determine if an expression fails:
Or to capture any condition as seen in rlang::catch_cnd
We can also create a variant of the try function that returns a list consisting of a result and error component.
safety <- function(expr) {
tryCatch(
error = function(cnd) {
list(result = NULL, error = cnd)
},
list(result = expr, error = NULL)
)
}
safety(log("x"))
#> $result
#> NULL
#> $error
#> <simpleError in log("x"): non-numeric argument to mathematical function>
safety(log(10))
#> $result
#> [1] 2.302585
#> $error
#> NULLHandlers can be used to make informative error messages and simulate options(warn = 2) to turn a warning into an error for a single block of code to help with debugging. You could write a similar function if you were trying to locate the source of a message.
Recording conditions for later investigation can be accomplished with conditions if an object is modified in place as calling handlers are called for their side effects and cannot return values. You can also capture errors by wrapping withCallingHandlers in a tryCatch.
catch_cnds <- function(expr) {
conds <- list()
add_cond <- function(cnd) {
conds <<- append(conds, list(cnd))
rlang::cnd_muffle(cnd)
}
tryCatch(
error = function(cnd) {
conds <<- append(conds, list(cnd))
},
withCallingHandlers(
message = add_cond,
warning = add_cond,
expr
)
)
conds
}
catch_cnds({
rlang::inform("a")
rlang::warn("b")
rlang::abort("c")
})
#> [[1]]
#> <message/rlang_message>
#> Message:
#> a
#> [[2]]
#> <warning/rlang_warning>
#> Warning:
#> b
#> [[3]]
#> <error/rlang_error>
#> Error:
#> ! c
#> ---
#> Backtrace:
#> ▆
#> 1. ├─global catch_cnds(...)
#> 2. │ ├─base::tryCatch(...)
#> 3. │ │ └─base (local) tryCatchList(expr, classes, parentenv, handlers)
#> 4. │ │ └─base (local) tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 5. │ │ └─base (local) doTryCatch(return(expr), name, parentenv, handler)
#> 6. │ └─base::withCallingHandlers(...)
#> 7. └─rlang::abort("c")The idea of recording conditions led to the creation of the evaluate R package which powers knitr. The functions in evaluate capture each output into a special data structure so it can be later replayed. The code is more complex as it needs to handle both plot and text output.
You can signal a condition that does not inherit from a message, warning, or error. In this case, there is no default behavior and the condition has no effect unless the user requests it. For example, you could create a logging system based on conditions:
The condition is signalled, but nothing happens with the code above as it has no default handler. To activate logging, you need a handler that does something with the log condition.
You could even add another function to selectively ignore some logging levels.
R for Lifestyle and Brain Health (R-LAB)