#> Error: ... in the name of love...
#> Error:
#> ! ...before you break my heart...
#> Error: ... think it o-o-over...
What are conditions? Problems that happen in functions:
As a function author, one can signal them–that is, say there’s a problem.
As a function consumer, one can handle them–for example, react or ignore.
Three types of conditions:
How to throw errors
#> Error: ... in the name of love...
#> Error:
#> ! ...before you break my heart...
#> Error: ... think it o-o-over...
Composing error messages
stop()
pastes together argumentsabort()
requires {glue}
May have multiple warnings per call
Print all warnings once call is complete.
#> Warning in warn(): This is your first warning
#> Warning in warn(): This is your second warning
#> Warning in warn(): This is your LAST warning
Like errors, warning()
has
{rlang}
analog#> Warning: Warning
#> Warning: Warning
#> Warning: Warning
(Hadley’s) advice on usage:
Mechanics:
Style:
Messages are best when they inform about:
A few ways:
try()
suppressWarnings()
suppressMessages()
try()
What it does:
#> Error in log(x) : non-numeric argument to mathematical function
#> [1] 10
Better ways to react to/recover from errors:
tryCatch()
to “catch” the error and perform a different action in the event of an error.#> Warning in file(file, "rt"): cannot open file 'possibly-bad-input.csv': No such
#> file or directory
suppressWarnings()
, suppressMessages()
What it does:
Every condition has a default behavior:
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
)
#> 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, use conditionMessage(cnd)
.call
. The function call that triggered the condition. To extract it, use conditionCall(cnd)
.But custom conditions may contain other components.
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.
#> [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.
Definition by verbal comparison:
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!
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
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
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: 0x000002ba6a952b98>
#> <environment: namespace:base>
#> 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: 0x000002ba6a952b98>
#> <environment: namespace:base>
#> 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: 0x000002ba6a952b98>
#> <environment: namespace:base>
The base::log()
function provides a minimal error message.
#> Error in log(letters): non-numeric argument to mathematical function
#> Error in log(1:10, base = letters): non-numeric argument to mathematical function
One could make a more informative error message about which argument is problematic.
Consider the difference:
#> Error in `my_log()`:
#> ! `x` must be a numeric vector; not character.
#> Error in `my_log()`:
#> ! `base` must be a numeric vector; not character.
Create a helper function to describe errors:
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", # <- this is the (error) class, I believe
message = msg,
arg = arg,
must = must,
not = not
)
}
Rewrite the log function to use this helper function:
See the result for the end user:
#> Error in `abort_bad_argument()`:
#> ! `x` must be numeric; not character.
#> Error in `abort_bad_argument()`:
#> ! `base` must be numeric; not character.
Use class of condition object to allow for different handling of different types of errors
tryCatch(
error_bad_argument = function(cnd) "bad_argument",
error = function(cnd) "other error",
my_log("a")
)
#> [1] "bad_argument"
But note that the first handler that matches any of the signal’s class, potentially in a vector of signal classes, will get control. So put the most specific handlers first.
See the sub-section in the book for excellent examples.