58

I'm using lapply to run a complex function on a large number of items, and I'd like to save the output from each item (if any) together with any warnings/errors that were produced so that I can tell which item produced which warning/error.

I found a way to catch warnings using withCallingHandlers (described here). However, I need to catch errors as well. I can do it by wrapping it in a tryCatch (as in the code below), but is there a better way to do it?

catchToList <- function(expr) {
  val <- NULL
  myWarnings <- NULL
  wHandler <- function(w) {
    myWarnings <<- c(myWarnings, w$message)
    invokeRestart("muffleWarning")
  }
  myError <- NULL
  eHandler <- function(e) {
    myError <<- e$message
    NULL
  }
  val <- tryCatch(withCallingHandlers(expr, warning = wHandler), error = eHandler)
  list(value = val, warnings = myWarnings, error=myError)
} 

Sample output of this function is:

> catchToList({warning("warning 1");warning("warning 2");1})
$value
[1] 1

$warnings
[1] "warning 1" "warning 2"

$error
NULL

> catchToList({warning("my warning");stop("my error")})
$value
NULL

$warnings
[1] "my warning"

$error
[1] "my error"

There are several questions here on SO that discuss tryCatch and error handling, but none that I found that address this particular issue. See How can I check whether a function call results in a warning?, warnings() does not work within a function? How can one work around this?, and How to tell lapply to ignore an error and process the next thing in the list? for the most relevant ones.

Jaap
  • 81,064
  • 34
  • 182
  • 193
Aaron left Stack Overflow
  • 36,704
  • 7
  • 77
  • 142

4 Answers4

61

Maybe this is the same as your solution, but I wrote a factory to convert plain old functions into functions that capture their values, errors, and warnings, so I can

test <- function(i)
    switch(i, "1"=stop("oops"), "2"={ warning("hmm"); i }, i)
res <- lapply(1:3, factory(test))

with each element of the result containing the value, error, and / or warnings. This would work with user functions, system functions, or anonymous functions (factory(function(i) ...)). Here's the factory

factory <- function(fun)
    function(...) {
        warn <- err <- NULL
        res <- withCallingHandlers(
            tryCatch(fun(...), error=function(e) {
                err <<- conditionMessage(e)
                NULL
            }), warning=function(w) {
                warn <<- append(warn, conditionMessage(w))
                invokeRestart("muffleWarning")
            })
        list(res, warn=warn, err=err)
    }

and some helpers for dealing with the result list

.has <- function(x, what)
    !sapply(lapply(x, "[[", what), is.null)
hasWarning <- function(x) .has(x, "warn")
hasError <- function(x) .has(x, "err")
isClean <- function(x) !(hasError(x) | hasWarning(x))
value <- function(x) sapply(x, "[[", 1)
cleanv <- function(x) sapply(x[isClean(x)], "[[", 1)
Martin Morgan
  • 45,935
  • 7
  • 84
  • 112
  • 4
    Yes, same idea, but much nicer! Have you considered wrapping it up into a package? From the other questions I saw just here on SO others would find this useful too. – Aaron left Stack Overflow Feb 10 '11 at 05:35
  • 1
    I have a function that stores its call in the output. After invoking `factory` this call is changed, e.g. `fun(formula = ..1, data = ..2, method = "genetic", ratio = ..4, print.level = 0)`, where `formula` should be my original input formula, but gets overwritten. Any tips? – Roman Luštrik Feb 25 '12 at 13:15
  • @RomanLuštrik: I'd guess it's because it's actually making a new function `fun` and calling that with the `...` instead of calling yours directly. I wonder if my `catchToList` function works, or if the `factory` could be modified, perhaps using `do.call`. How can it be reproduced? – Aaron left Stack Overflow Feb 28 '12 at 11:42
  • I really liked the approach of @russellpierce below for capturing warning and error messages using attributes. So the middle `NULL` above could be replaced with `NA` or `"An error occurred"` or similar, and the 2nd last line replaced with `attr(res,"warning") <- warn; attr(res,"error") <- err; return(res)` – JWilliman Nov 09 '18 at 00:17
  • When I try to use above for example with `f.log <- factory(log)` and then I do `isClean(f.log("a"))` or any other helper function I get: "Error in FUN(X[[i]], ...) : subscript out of bounds ". Same error if I do `isClean(f.log(10))` – Zak Keirn Feb 23 '19 at 16:49
  • @ZakKeirn the intended use was a function in lapply, e.g., `isClean(lapply(list(1, 2, "a"), f.log))` – Martin Morgan Feb 24 '19 at 00:44
21

Try the evaluate package.

library(evaluate)
test <- function(i)
    switch(i, "1"=stop("oops"), "2"={ warning("hmm"); i }, i)

t1 <- evaluate("test(1)")
t2 <- evaluate("test(2)")
t3 <- evaluate("test(3)")

It currently lacks a nice way of evaluating expression though - this is mainly because it's targetted towards reproducing exactly what R output's given text input at the console.

replay(t1)
replay(t2)
replay(t3)

It also captures messages, output to the console, and ensures that everything is correctly interleaved in the order in which it occurred.

hadley
  • 102,019
  • 32
  • 183
  • 245
  • 1
    Is there a way to capture the output of replay(t2) in this example, or in general? `x <- capture.output(replay(t1))` doesn't yield the full error message. Thanks. – wtrs Apr 18 '18 at 15:18
  • Can the following line be adjusted to work? `mutate(map(data, ~ evaluate("some_fn(.x)"))` – its.me.adam Feb 11 '22 at 02:46
  • @its.me.adam One solution is to define an evaluate function outside of mutate: `eval_fn <- function(x) evaluate("some_fn(x)")` then `mutate(map(data, ~ eval_fn(.x))) – its.me.adam Feb 11 '22 at 15:02
18

I have merged Martins soulution (https://stackoverflow.com/a/4952908/2161065) and the one from the R-help mailing list you get with demo(error.catching).

The main idea is to keep both, the warning/error message as well as the command triggering this problem.

myTryCatch <- function(expr) {
  warn <- err <- NULL
  value <- withCallingHandlers(
    tryCatch(expr, error=function(e) {
      err <<- e
      NULL
    }), warning=function(w) {
      warn <<- w
      invokeRestart("muffleWarning")
    })
  list(value=value, warning=warn, error=err)
}

Examples:

myTryCatch(log(1))
myTryCatch(log(-1))
myTryCatch(log("a"))

Output:

myTryCatch(log(1))
# $value
# [1] 0
# 
# $warning
# NULL
# 
# $error
# NULL

myTryCatch(log(-1))
# $value
# [1] NaN
# 
# $warning
# <simpleWarning in log(-1): NaNs produced>
#   
# $error
# NULL

myTryCatch(log("a"))
# $value
# NULL
# 
# $warning
# NULL
# 
# $error
# <simpleError in log("a"): non-numeric argument to mathematical function>
jay.sf
  • 60,139
  • 8
  • 53
  • 110
user2161065
  • 1,826
  • 1
  • 18
  • 18
  • It's nice but does not catch messages or prints. I would be nice with just a single function that caught all 4 main output types. I say main, because there are a few others such as plots, writing to clipboard and writing to file. In some cases, one would watch to catch these too. – CoderGuy123 Feb 15 '17 at 17:08
9

The purpose of my answer (and modification to Martin's excellent code) is so that the factory-ed function returns the data structure expected if all goes well. If a warning is experienced, it is attached to the result under the factory-warning attribute. data.table's setattr function is used to allow for compatibility with that package. If an error is experienced, the result is the character element "An error occurred in the factory function" and the factory-error attribute will carry the error message.

#' Catch errors and warnings and store them for subsequent evaluation
#'
#' Factory modified from a version written by Martin Morgan on Stack Overflow (see below).  
#' Factory generates a function which is appropriately wrapped by error handlers.  
#' If there are no errors and no warnings, the result is provided.  
#' If there are warnings but no errors, the result is provided with a warn attribute set.
#' If there are errors, the result retutrns is a list with the elements of warn and err.
#' This is a nice way to recover from a problems that may have occurred during loop evaluation or during cluster usage.
#' Check the references for additional related functions.
#' I have not included the other factory functions included in the original Stack Overflow answer because they did not play well with the return item as an S4 object.
#' @export
#' @param fun The function to be turned into a factory
#' @return The result of the function given to turn into a factory.  If this function was in error "An error as occurred" as a character element.  factory-error and factory-warning attributes may also be set as appropriate.
#' @references
#' \url{http://stackoverflow.com/questions/4948361/how-do-i-save-warnings-and-errors-as-output-from-a-function}
#' @author Martin Morgan; Modified by Russell S. Pierce
#' @examples 
#' f.log <- factory(log)
#' f.log("a")
#' f.as.numeric <- factory(as.numeric)
#' f.as.numeric(c("a","b",1))
factory <- function (fun) {
  errorOccurred <- FALSE
  library(data.table)
  function(...) {
    warn <- err <- NULL
    res <- withCallingHandlers(tryCatch(fun(...), error = function(e) {
      err <<- conditionMessage(e)
      errorOccurred <<- TRUE
      NULL
    }), warning = function(w) {
      warn <<- append(warn, conditionMessage(w))
      invokeRestart("muffleWarning")
    })
    if (errorOccurred) {
      res <- "An error occurred in the factory function"
    } 

    if (is.character(warn)) {
      data.table::setattr(res,"factory-warning",warn)
    } else {
      data.table::setattr(res,"factory-warning",NULL) 
    }

    if (is.character(err)) {
      data.table::setattr(res,"factory-error",err)
    } else {
      data.table::setattr(res, "factory-error", NULL)
    }  
    return(res)
  }
}

Because we don't wrap the result in an extra list we can't make the kind of assumptions that allow for some of his accessor functions, but we can write simple checks and decide how to handle the cases as is appropriate to our particular resulting data-structure.

.has <- function(x, what) {
  !is.null(attr(x,what))
}
hasWarning <- function(x) .has(x, "factory-warning")
hasError <- function(x) .has(x, "factory-error")
isClean <- function(x) !(hasError(x) | hasWarning(x))
russellpierce
  • 4,583
  • 2
  • 32
  • 44
  • f.log(10) from the example prints "An error occurred in the factory function" instead of returning the result as I expected. However the isClean() reports properly as TRUE. So can you get the return value or is this only for error and warning checking? – Zak Keirn Feb 23 '19 at 16:27
  • @ZakKeirn I'm not able to replicate your problem using R 3.5.1. Please double check that there wasn't a little snafu in copying and pasting. If the problem persists, please share more information about your execution environment. – russellpierce Mar 02 '19 at 20:56
  • R version is 3.5.2. I just copied, pasted, and ran again. If the first time I run `f.log(10)`, it works and returns a value. Then if I run `f.log("a")` and then run again with `f.log(10)` after that, it comes back with "An error occurred in the factory function". – Zak Keirn Mar 04 '19 at 15:22
  • Ah, that's a good hint. The closure is getting updated. I'd only used this for multicore cases where each copy of the function was envoked once. I'll take a look and see if there is a fix. Probably updating the error variable to FALSE when starting up – russellpierce Mar 04 '19 at 15:25