8.4 Handling conditions
Every condition has a default behavior:
- ❌ Errors halt execution
- ⚠️ Warnings are collected during execution and displayed in bulk after execution
- 💬 Messages are displayed immediately
Condition handlers allow one to change that behavior (within the scope of a function).
Two handler functions:
tryCatch()
withCallingHandlers()
# try to run `code_to_try_to_run`
# if (error) condition is signalled, fun some other code
tryCatch(
error = function(cnd) {
# code to run when error is thrown
},
code_to_try_to_run
)
# try to `code_to_try_to_run`
# if condition is signalled, run code corresponding to condition type
withCallingHandlers(
warning = function(cnd) {
# code to run when warning is signalled
},
message = function(cnd) {
# code to run when message is signalled
},
code_to_try_to_run
)
8.4.1 Condition objects
# catch a condition
cnd <- rlang::catch_cnd(stop("An error"))
# inspect it
str(cnd)
#> List of 2
#> $ message: chr "An error"
#> $ call : language force(expr)
#> - attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
The standard components
message
. The error message. To extract it, useconditionMessage(cnd)
.call
. The function call that triggered the condition. To extract it, useconditionCall(cnd)
.
But custom conditions may contain other components.
8.4.2 Exiting handlers
If a condition is signalled, this type of handler controls what code to run before exiting the function call.
f3 <- function(x) {
tryCatch(
# if error signalled, return NA
error = function(cnd) NA,
# try to run log
log(x)
)
}
f3("x")
#> [1] NA
When a condition is signalled, control moves to the handler and never returns to the original code.
tryCatch(
message = function(cnd) "There",
{
message("Here")
stop("This code is never run!")
}
)
#> [1] "There"
The tryCatch()
exit handler has one final argument: finally
. This is run regardless of the condition of the original code. This is often used for clean-up.
8.4.3 Calling handlers
Definition by verbal comparison:
- With exit handlers, code exits the normal flow once a condition is signalled
- With calling handlers, code continues in the normal flow once control is returned by the handler.
Definition by code comparison:
# with an exit handler, control moves to the handler once condition signalled and does not move back
tryCatch(
message = function(cnd) cat("Caught a message!\n"),
{
message("Someone there?")
message("Why, yes!")
}
)
#> Caught a message!
# with a calling handler, control moves first to the handler and the moves back to the main code
withCallingHandlers(
message = function(cnd) cat("Caught a message!\n"),
{
message("Someone there?")
message("Why, yes!")
}
)
#> Caught a message!
#> Someone there?
#> Caught a message!
#> Why, yes!
8.4.4 By default, conditions propagate
Let’s suppose that there are nested handlers. If a condition is signalled in the child, it propagates to its parent handler(s).
# Bubbles all the way up to default handler which generates the message
withCallingHandlers(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
#> Level 1
#> Level 2
#> Hello
# Bubbles up to tryCatch
tryCatch(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
#> Level 1
#> Level 2
8.4.5 But conditions can be muffled
If one wants to “muffle” the siginal, one needs to use rlang::cnd_muffle()
# Muffles the default handler which prints the messages
withCallingHandlers(
message = function(cnd) {
cat("Level 2\n")
rlang::cnd_muffle(cnd)
},
withCallingHandlers(
message = function(cnd) cat("Level 1\n"),
message("Hello")
)
)
#> Level 1
#> Level 2
# Muffles level 2 handler and the default handler
withCallingHandlers(
message = function(cnd) cat("Level 2\n"),
withCallingHandlers(
message = function(cnd) {
cat("Level 1\n")
rlang::cnd_muffle(cnd)
},
message("Hello")
)
)
#> Level 1
8.4.6 Call stacks
Call stacks of exiting and calling handlers differ.
Why?
Calling handlers are called in the context of the call that signalled the condition exiting handlers are called in the context of the call to tryCatch()
To see this, consider how the call stacks differ for a toy example.
# create a function
f <- function() g()
g <- function() h()
h <- function() message
# call stack of calling handlers
withCallingHandlers(f(), message = function(cnd) {
lobstr::cst()
rlang::cnd_muffle(cnd)
})
#> function (..., domain = NULL, appendLF = TRUE)
#> {
#> cond <- if (...length() == 1L && inherits(..1, "condition")) {
#> if (nargs() > 1L)
#> warning("additional arguments ignored in message()")
#> ..1
#> }
#> else {
#> msg <- .makeMessage(..., domain = domain, appendLF = appendLF)
#> call <- sys.call()
#> simpleMessage(msg, call)
#> }
#> defaultHandler <- function(c) {
#> cat(conditionMessage(c), file = stderr(), sep = "")
#> }
#> withRestarts({
#> signalCondition(cond)
#> defaultHandler(cond)
#> }, muffleMessage = function() NULL)
#> invisible()
#> }
#> <bytecode: 0x557c29c19580>
#> <environment: namespace:base>
# call stack of exit handlers
tryCatch(f(), message = function(cnd) lobstr::cst())
#> function (..., domain = NULL, appendLF = TRUE)
#> {
#> cond <- if (...length() == 1L && inherits(..1, "condition")) {
#> if (nargs() > 1L)
#> warning("additional arguments ignored in message()")
#> ..1
#> }
#> else {
#> msg <- .makeMessage(..., domain = domain, appendLF = appendLF)
#> call <- sys.call()
#> simpleMessage(msg, call)
#> }
#> defaultHandler <- function(c) {
#> cat(conditionMessage(c), file = stderr(), sep = "")
#> }
#> withRestarts({
#> signalCondition(cond)
#> defaultHandler(cond)
#> }, muffleMessage = function() NULL)
#> invisible()
#> }
#> <bytecode: 0x557c29c19580>
#> <environment: namespace:base>
tryCatch(f(), message = function(cnd) lobstr::cst())
#> function (..., domain = NULL, appendLF = TRUE)
#> {
#> cond <- if (...length() == 1L && inherits(..1, "condition")) {
#> if (nargs() > 1L)
#> warning("additional arguments ignored in message()")
#> ..1
#> }
#> else {
#> msg <- .makeMessage(..., domain = domain, appendLF = appendLF)
#> call <- sys.call()
#> simpleMessage(msg, call)
#> }
#> defaultHandler <- function(c) {
#> cat(conditionMessage(c), file = stderr(), sep = "")
#> }
#> withRestarts({
#> signalCondition(cond)
#> defaultHandler(cond)
#> }, muffleMessage = function() NULL)
#> invisible()
#> }
#> <bytecode: 0x557c29c19580>
#> <environment: namespace:base>