Is there a way to `source()` and continue after an error?

I have a large R script of, say, 142 small sections. If one section fails with an error I'd like the script to continue rather than halt. The sections don't necessarily depend on each other, but some do. If one in the middle fails that's ok. I'd prefer not to pepper this script with try() calls. And I'd prefer not to have to split the file up into many smaller files as each section is quite short.

If source() could be made to work as if it had been copy and pasted at the R console, that would be great. Or if there was a way to downgrade error to warning, that would be fine too.

Once the script has run I intend to grep (or similar) the output for error or warning text so that I can see all the errors or warnings that have occurred, not just that it has halted on the first error.

I've read ?source and searched Stack Overflow's [R] tag. I found the following similar questions, but try and tryCatch were the answers provided :

R Script - How to Continue Code Execution on Error
Is there any way to have R script continue after receiving error messages instead of halting execution?

I'm not looking for try or tryCatch for the reasons above. This isn't for R package testing, where I'm aware of the testing frameworks and where many try() or test_that() calls (or similar) are entirely appropriate. This is for something else where I have a script as described.

Thanks!


Solution 1:

To make this more concrete, how about the following?

First, to test the approach, create a file (call it "script.R") containing several statements, the first of which will throw an error when evaluated.

## script.R

rnorm("a")
x <- 1:10
y <- 2*x

Then parse it into a expression list, and evaluate each element in turn, wrapping the evaluation inside a call to tryCatch() so that errors won't cause too much damage:

ll <- parse(file = "script.R")

for (i in seq_along(ll)) {
    tryCatch(eval(ll[[i]]), 
             error = function(e) message("Oops!  ", as.character(e)))
}
# Oops!  Error in rnorm("a"): invalid arguments
# 
# Warning message:
# In rnorm("a") : NAs introduced by coercion
x
# [1]  1  2  3  4  5  6  7  8  9 10
y
# [1]  2  4  6  8 10 12 14 16 18 20

Solution 2:

The evaluate package supplies another option with its evaluate() function. It's not as light-weight as my other suggestion, but -- as one of the functions underpinning knitr -- its been as well tested as you could hope for!

library(evaluate)

rapply((evaluate(file("script.R"))), cat)  # For "script.R", see my other answer
# rnorm("a")
# NAs introduced by coercionError in rnorm("a") : invalid arguments
# In addition: Warning message:
# In rnorm("a") : NAs introduced by coercion
x
#  [1]  1  2  3  4  5  6  7  8  9 10
y
#  [1]  2  4  6  8 10 12 14 16 18 20

For output that looks more like you had actually typed the statements in at the command-line, use replay(). (Thanks to hadley for the tip):

replay(evaluate(file("script.R")))
# >
# > rnorm("a")
# Warning message:
# NAs introduced by coercion
# Error in rnorm("a"): invalid arguments
# > x <- 1:10
# > y <- 2*x

Edit

replay() also offers a better way to play back just the errors and warnings, if those are all that you need:

## Capture the output of evaluate()
res <- evaluate(file("script.R"))

## Identify those elements inheriting from "error" or "warning
ii <- grepl("error|warning", sapply(res, class))   

## Replay the errors and warnings
replay(res[ii])
# Warning message:
# NAs introduced by coercion
# Error in rnorm("a"): invalid arguments
# > 

Solution 3:

this is clunky and uses nobody's friend eval(parse( but might be somewhat helpful.. josh's answer is much cleaner.

# assign the filepath
fn <- "c:/path/to your/script.R"

# read in the whole script
z <- readLines( fn )

# set a starting j counter
j <- 1

# loop through each line in your script..
for ( i in seq(length(z)) ) {

    # catch errors
    err <- try( eval( parse( text = z[j:i] ) ) , silent = TRUE )

    # if it's not an error, move your j counter up to one past i
    if ( class( err ) != 'try-error' ) j <- i + 1 else

    # otherwise, if the error isn't an "end of input" error,
    # then it's an actual error and needs to be just plain skipped.
    if ( !grepl( 'unexpected end of input' , err ) ) j <- i + 1

    # otherwise it's an "end of line" error, which just means j alone.
}