Statistics, Science, Random Ramblings

A blog about data and other interesting things

Error handling and assertions in R

Posted at — Apr 14, 2022

There are many situations in which you should handle errors properly or ensure they do not happen in the first place when using R. This is especially true if you automate long-running tasks or when writing code other people use (like a package).

Signalling errors

R allows you to manually signal that an error occurred by calling stop.

testX <- function(x) {
    if (is.character(x)) {
        stop("x must not be of type character")
    }
}
testX("abc")
## Error in testX("abc"): x must not be of type character

Unlike in other languages like Python there are no different types of errors in R; there is nothing like TypeError or ValueError. All errors you signal with stop are the same and you should be specific (but concise) with the error message.

However, sometimes an error might be too much for what you are trying to do, so you might want to use the less severe levels of signalling: warning and message.

squareX <- function(x) {
    if (is.character(x)) {
        warning("Converting x to numeric")
        x <- as.numeric(x)
    } else {
        # the type checking done here is of course very incomplete
        message("x appears to be numeric")
    }
    x ^ 2 
}
squareX("4")
## Warning in squareX("4"): Converting x to numeric
## [1] 16
squareX(5)
## x appears to be numeric
## [1] 25

In both cases execution of the code will continue, but the warning or message is displayed to the user. You should probably ensure that you do not use warning and message excessively, otherwise users of your code might be tempted to call suppressWarnings or suppressMessages, kind of defeating the purpose of your warnings and messages.

stopifnot for assertions

The pattern in our very first example can be expressed more elegantly using stopifnot. This function does exactly what the name suggests: it stops if a condition is not met.

testX <- function(x) {
    stopifnot(
        x > 0, 
        is.numeric(x)
    )
}
testX("abc")
## Error in testX("abc"): is.numeric(x) is not TRUE
testX(-10)
## Error in testX(-10): x > 0 is not TRUE

You can provide many conditions to stopifnot and code execution will only continue if all of these are met. Typically, stopifnot is more suitable for using at the beginning of your function, while calling stop is better suited for using deeper within your function. The main issue here is that you can control the message outputted by stop, but not the message stopifnot generates. As stopifnot clearly refers to the variable that was checked it can be confusing if your users see errors referring to internal variables of your function.

Ignoring errors with try

Sometimes you want to keep going when an error occurs. For example when you run a job that processes a bunch of data sets and it takes a while to finish, you might want to ensure that things keep going when the processing of a single data set fails. If you have a job that you expect will take a few days to finish and you then have look at it after a while and realise it failed after a few hours, that is not exactly a nice feeling. Or so I have heard…

This is where try is useful. If whatever is wrapped in try throws an error, try will return an object of class try-error and your code will keep on going afterwards.

err <- try(mean())
## Error in mean.default() : argument "x" is missing, with no default
err
## [1] "Error in mean.default() : argument \"x\" is missing, with no default\n"
## attr(,"class")
## [1] "try-error"
## attr(,"condition")
## <simpleError in mean.default(): argument "x" is missing, with no default>

If you use something like lapply to run your job it will be very easy to filter out the items where the processing failed by omitting all items which have the try-error class. While try is very simple, used in the right places it can make your life a lot easier.

Being more fine grained: tryCatch and withCallingHandlers

Sometimes you need a bit more granularity in handling conditions. For example you might want to also be able to handle warnings and log messages somewhere. This is where tryCatch and withCallingHandlers come in. Both are very similar, but behave slightly differently.

Let’s first look at tryCatch:

my_function <- function() {
    mean(letters) 
    x <- mean(1:10)
    x
}

tryCatch(
    expr = { 
        my_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("Something does not look right.")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "Something does not look right."

Our not very useful function does generate a warning in its first statement.1 This warning is handled by tryCatch and the execution of our function is halted an that point.

Now, we will do the same with withCallingHandlers:

withCallingHandlers(
    expr = { 
        my_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("something does not look right")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "something does not look right"
## Warning in mean.default(letters): argument is not numeric or logical: returning
## NA
## [1] 5.5

We spot to crucial differences:

  1. Our function keeps going after the warning was thrown. Whatever we specify in warning is executed, but our function keeps going.
  2. The condition is not consumed by withCallingHandlers, we do see the warning that was raised.

This probably makes you think that if the condition is raised again by withCallingHandlers, what happens when there is an error?

Let’s first try this with tryCatch:

my_new_function <- function() {
    stop("oh no :(")
    print("this line will never be printed")
}

tryCatch(
    expr = { 
        my_new_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("something does not look right")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "An error happened."

Everything acts as expected. We do not get an error, but it is handled and consumed. So, something gets wrong and it is handled, pretty much what error handling sounds like it should do.

Now, let’s try again with withCallingHandlers:

withCallingHandlers(
    expr = { 
        my_new_function()
    }, 
    error = function(e) {
        print("An error happened.")
    },
    warning = function(e) {
        print("something does not look right")
    },
    message = function(e) {
        print("You should make sure results are correct.")
    }
)
## [1] "An error happened."
## Error in my_new_function(): oh no :(

And indeed we do error out.

So, you might want to use withCallingHandlers only for dealing with messages and warnings, but probably not with errors, for which tryCatch is more suitable.

And as you might have guessed by now, you can combine both to gracefully handle all conditions.

tryCatch({
    withCallingHandlers(
        expr = {
            my_new_function()
        },
        error = function(e) {
            print("An error occured")
        }
    )},
    error = function(e) {
        print("Failing with style")
    }
)
## [1] "An error occured"
## [1] "Failing with style"

At this point the code becomes a bit convoluted, but it does the job.

Defining your own conditions

R allows you to define your own conditions if you really want to do some very specific error handling. It is kind of straightforward by creating an object that has the class condition.

my_condition <- function(msg = "my condition") {
    structure(
        class = c("my_condition", "condition"),
        list(message = msg)
    )
}

Of course you are free to add whatever more information you would like to include in your class, having a look at ?sys.call might be worthwhile for example.

You can use your shiny new condition like the default ones as well.

withCallingHandlers(
    expr = {
        signalCondition(my_condition())
        print("the above condition was handled")
    },
    my_condition = function(e) {
        print("handling condition...")
    }
)
## [1] "handling condition..."
## [1] "the above condition was handled"

But I suppose it is kind of rare that there is need to build your own conditions with R.

Packages for condition handling and assertions

An alternative to stopifnot is the assertthat package.

foo <- function(x) {
    assertthat::assert_that(x == 1, msg = "x must always be 1")
    "yay"
} 
foo(1)
## [1] "yay"
foo(2)
## Error: x must always be 1

The package provides a bunch of helpers to check various conditions.

There is also the tryCatchLog package providing alternatives to try and tryCatch.

tryCatchLog::tryLog(stop("error :("))
## ERROR [2022-04-14 12:36:03] error :(
## 
## Compact call stack:
##   1 local({
## 
## Full call stack:
##   1 local({
##         if (length(a <- commandArgs(TRUE)) != 2) 
##             stop("T
##   2 eval.parent(substitute(eval(quote(expr), envir)))
##   3 eval(expr, p)
##   4 eval(expr, p)
##   5 eval(quote({
##         if (length(a <- commandArgs(TRUE)) != 2) stop("The numb
##   6 eval(quote({
##         if (length(a <- commandArgs(TRUE)) != 2) stop("The numb
##   7 do.call(f, x[[2]], envir = globalenv())
##   8 (function (input, output, to_md = file_ext(output) != "html", quiet = TRUE)
##   9 rmarkdown::render(input, "blogdown::html_page", output_file = output, envir
##   10 knitr::knit(knit_input, knit_output, envir = envir, quiet = quiet)
##   11 process_file(text, output)
##   12 withCallingHandlers(if (tangle) process_tangle(group) else process_group(gr
##   13 process_group(group)
##   14 process_group.block(group)
##   15 call_block(x)
##   16 block_exec(params)
##   17 eng_r(options)
##   18 in_dir(input_dir(), evaluate(code, envir = env, new_device = FALSE, keep_wa
##   19 evaluate(code, envir = env, new_device = FALSE, keep_warning = !isFALSE(opt
##   20 evaluate::evaluate(...)
##   21 evaluate_call(expr, parsed$src[[i]], envir = envir, enclos = enclos, debug 
##   22 timing_fn(handle(ev <- withCallingHandlers(withVisible(eval(expr, envir, en
##   23 handle(ev <- withCallingHandlers(withVisible(eval(expr, envir, enclos)), wa
##   24 try(f, silent = TRUE)
##   25 tryCatch(expr, error = function(e) {
##         call <- conditionCall(e)
##        
##   26 tryCatchList(expr, classes, parentenv, handlers)
##   27 tryCatchOne(expr, names, parentenv, handlers[[1]])
##   28 doTryCatch(return(expr), name, parentenv, handler)
##   29 withCallingHandlers(withVisible(eval(expr, envir, enclos)), warning = wHand
##   30 withVisible(eval(expr, envir, enclos))
##   31 eval(expr, envir, enclos)
##   32 eval(expr, envir, enclos)
##   33 tryCatchLog::tryLog(stop("error :("))
##   34 tryCatchLog(expr = expr, execution.context.msg = execution.context.msg, wri
##   35 tryCatch(withCallingHandlers(expr, condition = cond.handler), ..., finally 
##   36 tryCatchList(expr, classes, parentenv, handlers)
##   37 tryCatchOne(expr, names, parentenv, handlers[[1]])
##   38 doTryCatch(return(expr), name, parentenv, handler)
##   39 withCallingHandlers(expr, condition = cond.handler)
##   40 stop("error :(")
##   41 .handleSimpleError(function (c) 
##     {
##         if (inherits(c, "condition") &

It does not only provide detailed call stacks when something goes wrong, it also makes it straightforward to log conditions to a file.

So, what should you use?

To be honest, in the vast majority of cases simply using try and stopifnot is enough to make sure nothing goes terribly wrong.

When things are more complicated the tryCatchLog package is probably worth a look, because when there is that much need for elaborate handling of conditions the features it adds are probably very welcome.


  1. For some reason it is very hard to get an error from functions like mean, sd or median. If they get non-numeric input they will return NA and generate a warning, but I would really prefer if they there was an option to make them generate an error in these cases.↩︎