Operational systems, by definition, need to work without human input. Systems are considered “operational” after they have been thoroughly tested and shown to work properly with a variety of input.
However, no software is perfect and no real-world system operates with 100% availability or 100% consistent input. Things occasionally go wrong – perhaps intermittently. In a situation with occasional failures it is vitally important to have robust error handling to deal with both expected and unexpected results.
Other languages used in operational settings have language statements to help with error handling. The code to handle errors looks very similar in java and python:
java
try {
myFunc(a)
} catch (abcException e) {
// handle abcException
} catch (defException e) {
// handle defException
} finally {
// always executed after handlers
}
python
try:
myFunc(a)
except abcError:
# handle abcError
except defError:
# handle defError
finally:
# always executed after handlers
Not surprisingly, a functional language like R does things differently:
tryCatch()
is a functionNevertheless, R’s error handling functions can be made to look similar to java and python:
result <- tryCatch({
myFunc(a)
}, warning = function(w) {
# handle all warnings
}, error = function(e) {
# handle all errors
}, finally = {
# always executed after handlers
}
For more details see:
In our experience, R’s error handling is too complicated for simple use and requires too much from folks who don’t consider themselves R-gurus.
Instead, we recommend wrapping any block of code that needs error
handling in a try()
function and then testing the result to
see if an error occurred.
try()
is a wrapper around tryCatch()
try()
ignores warningstry()
returns a “try-error” object on errorgeterrmessage()
returns latest error msgFor more details see:
This strategy makes it easy to create error handling logic and easy
to understand what it does. In the following pseudo-code please note
that R considers everything between {}
to be a single
expression:
result <- try({
# ...
# lines of R code
# ...
}, silent = TRUE)
if ( "try-error" %in% class(result) ) {
err_msg <- geterrmessage()
# logging of error message
# detection and handling of particular error strings
# stop() if necessary with user friendly error strings
}
stopOnError()
The stopOnError()
utility function
regularizes our handling of errors in operational code. This function
tests the first argument for a class of try-error
and, if
true, performs the following actions:
err_msg
from a user provided error message or,
if NULL, geterrmessage()
prefix
and maxLength
logger.error(err_msg)
if logging
has been enabledstop(err_msg)
Encouraging junior R programmers to add error handling to their code is now much easier. They can place any block of R code within a “try block” with the following minimal syntax:
result <- try({
# ...
# lines of R code
# ...
}, silent = FALSE)
stopOnError(result)
Using the %>%
pipe operator, we can write this even
more concisely without creating the interim result
object:
try({
# ...
# lines of R code
# ...
}, silent = FALSE) %>%
stopOnError()
Here is a working example demonstrating how a web service might test
for user input that may not have been converted from character to
numeric. All errors are appropriately logged. (The outer
try()
blocks in the examples below allow the code to be
evaluated for this vignette.).
In the third example, we see how low level error messages that may be hard to understand in the context of a complex, multi-level piece of code can be converted into a message that makes sense in the context of a web service application.
library(MazamaCoreUtils)
logger.setup()
logger.setLevel(TRACE) # force logs to be printed to the console
# Arbitrarily deep in the stack we might have:
myFunc <- function(x) {
return(log(x))
}
# ----- Example 1: good user input --------------------------------------------
try({
userInput <- 10
logger.trace("class(userInput) = %s", class(userInput))
try({
myFunc(x = userInput)
}, silent = TRUE) %>%
stopOnError()
logger.trace("Continue processing ...")
}, silent = TRUE)
#> TRACE [2024-02-07 10:48:01] class(userInput) = numeric
#> TRACE [2024-02-07 10:48:01] Continue processing ...
# ----- Example 2: bad user input ---------------------------------------------
try({
userInput <- "10"
logger.trace("class(userInput) = %s", class(userInput))
try({
myFunc(x = userInput)
}, silent = TRUE) %>%
stopOnError()
logger.trace("Continue processing ...") # we don't get here
}, silent = TRUE)
#> TRACE [2024-02-07 10:48:01] class(userInput) = character
#> ERROR [2024-02-07 10:48:01] Error in log(x) : non-numeric argument to mathematical function
# ----- Example 3: bad user input, custom error message -----------------------
try({
try({
logger.trace("class(userInput) = %s", class(userInput))
myFunc(x = userInput)
}, silent = TRUE) %>%
stopOnError("Unable to process user input")
logger.trace("Continue processing ...") # we don't get here
}, silent = TRUE)
#> TRACE [2024-02-07 10:48:01] class(userInput) = character
#> ERROR [2024-02-07 10:48:01] Unable to process user input