33

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!

Community
  • 1
  • 1
Matt Dowle
  • 58,872
  • 22
  • 166
  • 224
  • Is there any reason not to make your own callable REPL with `parse` and `eval`? – Matthew Plourde Jan 30 '13 at 19:48
  • @MatthewPlourde Not especially I guess. Maybe multi line statements and quoting might cause issues. Interesting idea. – Matt Dowle Jan 30 '13 at 19:59
  • Are you expecting failures because of errors in the code you wrote or because of the data the code snippets are processing? Why are you "bundling" sections which do *not* depend on each other? This whole setup seems to violate a few Prime Directives of Good Coding Practice. Why not write a script which (possibly loops) calls `source` on each sub-group of sections? Then one crash won't affect subsequent calls to `source` . – Carl Witthoft Jan 30 '13 at 21:08
  • 2
    @CarlWitthoft Answers in order: (1) both, and code other people wrote, and that me or other people might write or change in future. (2) it's a report so there is an order to it (3) because there are 142 (say) sections - I don't want to split it up and I don't want (or need) to think too hard about what the dependencies are or how they might change over time. I'll of course clean up all warnings and errors, but in the meantime I don't want side issues to hold it up. The report includes the warnings and errors (if any) both inline and in summary - they aren't hidden or forgotten. – Matt Dowle Jan 30 '13 at 21:40

3 Answers3

28

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
Josh O'Brien
  • 159,210
  • 26
  • 366
  • 455
  • +1 Looks very promising, thanks. Looks like it should cope with multi line statements. Will give it a go ... – Matt Dowle Jan 30 '13 at 20:16
  • @MatthewDowle -- Thanks. FYI, I just edited the answer slightly, adding a call to `as.character.error()` so that error messages are preserved in the final output. – Josh O'Brien Jan 30 '13 at 20:28
  • 1
    Have tested and this is perfect. Thanks again! – Matt Dowle Jan 30 '13 at 20:40
  • 3
    wish I could like that more – gaut Nov 12 '18 at 09:11
  • 1
    What happens if the parser throws an error though? For example, if someone writes `1::3` in a script, and you try to parse it, it fails: `parse(text = "1::3")` @JoshO'Brien – Taylor May 20 '20 at 16:20
  • there is some mention of "partial parsing" at the end of the documentation of `parse`, but I'm still having a difficult time with it, personally – Taylor May 20 '20 at 17:29
  • @Taylor -- I second this as an issue. My parser is throwing an error in my version of the `ll <- parse(file = "script.R")` line. My workaround has been to manually comment out all known errors, but the `parse()` function solution does not solve the problem. – kamiks Sep 13 '22 at 02:21
15

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
# > 
Josh O'Brien
  • 159,210
  • 26
  • 366
  • 455
  • 2
    I think you want `replay(evaluate(file("script.R")))`, and see the `stop_on_error` argument to `evaluate`. The evaluate package powers knitr, so it's fairly well tested. – hadley Jan 30 '13 at 20:46
  • @MatthewDowle also, with the evaluate package, the results of `evaluate` have s3 classes so you can easily look for errors, warnings, messages, etc. – hadley Jan 30 '13 at 20:48
  • Ahah so that's what `evaluate` does. Will take a look. Thanks @hadley ! – Matt Dowle Jan 30 '13 at 21:48
  • @hadley I tried `evaluate`, it's neat. `replay(evaluate(file("script.r")))` works and I see each line at the prompt as if it had been pasted. Is there a way to stop that echo? I'd like it to run as `source` does (silently), but displaying any errors or warnings and continuing after them. The code is writing results to a report, it isn't the report itself, if that helps to know. – Matt Dowle Jan 31 '13 at 16:31
  • @MatthewDowle -- How about this (using a sample file from the **evaluate** package): `samples <- system.file("tests", package = "evaluate"); res <- evaluate(file(file.path(samples, "order.r"))); replay(res[grepl("error|warning", sapply(res, class))])`. This is what Hadley's comment about the results having S3 classes was aimed at, methinks ;) (Also, it must be how **knitr** is able to implement separate hooks for treating/decorating errors, warnings, messages, etc.) – Josh O'Brien Jan 31 '13 at 16:48
  • 1
    @hadley is there any way to make `evaluate` actually store the output of the executed code in an environment of my choosing, instead of just the warning and error messages? – Taylor May 20 '20 at 17:02
  • @hadley, I second @Taylor 's question. Say you create scripts with lines that will purposefully trigger errors and then want to `source()` those scripts from a master script. This proposed solution does not seem to allow you to source a script and effectively skip over expressions that may trigger an error. You can of course manually comment out the errors, but seeking a solution other than that. – kamiks Sep 13 '22 at 02:25
5

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.
}
Anthony Damico
  • 5,779
  • 7
  • 46
  • 77