Signalling (throwing) and Handling (catching) Custom Conditions (exceptions) in R Ⅱ : R Termination and Resumption Handlers

In the first part of this series, we saw that there are two kinds of handlers : termination handlers and resumption handlers. Termination handlers are used by many languages such as C++ or Java. Resumption handler, on the other end, are more common among functional languages.

Condition Handling in R

R provides for both termination and resumption handlers. And R being Common Lisp in an Awk disguise, its view on unusual state of a program is more general than what we have seen so far as. Errors are only one among other conditions that includes messages and warnings as well as interrupts. Also, depending on the condition handler you’re using, conditions are either handled by transferring control to the place where the condition is caught (termination syntax) or evaluated in the context where the condition was signalled optionally transferring control there (resumption syntax).

And, in addition to that, R uses functions to handle conditions instead of statements like other programming languages do.

In R, conditions are signalled by either signalCondition() or stop() and there are subtle differences between the two as we will later see. Also, conditions are handled either with tryCatch() function or the withCallingHandlers() function.

Basically, calling the function

stop("Something Wicked This Way Comes")

will interrupt the execution of the script and either returns control to the top environment in interactive mode or causes R to exit in batch mode.

stop() usually takes an error message as its first argument. But it also accepts a condition object which is a lesser known but useful feature as we’re about to see.

stop(errorCondition(message="Division by zero", class="divisionByZero"))

The errorCondition()function creates an S3 object inheriting from classes condition and error. Actually, calling stop() with an error message as argument is equivalent to

stop(errorCondition(message="error", class="simpleError")

base R also provide the stopifnot()function

## IEEE 754 64 bits floats mantissa are 53 bit wide. Adding 1 to 2^53 causes a loss of precision
stopifnot( ( 2^53+1 )==2^53, "we are losing precision") 

which is a functional equivalent to assert in other languages.

The difference between signalCondition() and stop() is that signalCondition() only signals a condition but does not stop it :

(function(){ signalCondition("whatever");T})()
(function(){ stop("whatever");T } )()
TRUE
Erreur dans (function() { : whatever

While calling stop() breaks the flow of execution of the function and returns to the global environment, signalCondition() itself does not alter the flow of th program. You need to use signalCondition() in conjunction with tryCatch() or withCallingHandlers() otherwise nothing happens.

##
tryCatch(
  (function(){ signalCondition(errorCondition("whatever", class="whatever"));T})()
  , whatever = function(e){cat("Caught whatever\n");e}
)
##
withCallingHandlers(
  (function(){ signalCondition(errorCondition("whatever", class="whatever"));T})()
  , whatever = function(e){cat("Caught whatever\n");e}
)
Caught whatever
<whatever: whatever>

Caught whatever
[1] TRUE

Termination Handler in R

Java–like condition handling is provided by the tryCatch() function.

rv <- tryCatch({stop("AH-AH!")}, error = function(e) e)
##
class(rv)
##
conditionMessage(rv)
> class(rv)
[1] "simpleError" "error"       "condition"  
> conditionMessage(rv)
[1] "AH-AH!"

As stated before, calling stop() with an error message as argument creates a simpleError object inheriting from the condition and error classes.

If an error is thrown by stop(), tryCatch() passes the condition object to the anonymous function function(e) e which, in this case, simply returns the condition object e which, in turn, is assigned to the rv variable.

tryCatch() also catches message or warning

tryCatch(
  <expressions>
  , message = function(m){
    ## handle message
  }
  , warning = function(w){
    ## handle warning
  }
  , error = function(e){
    ## handle error
  }
)

But, little known is that the names are not limited to message, warning or error. You can actually use any condition name created with the errorCondition() function

rv <- tryCatch(
  stop(
    errorCondition(
      message = "file not found."
      , class= 'fileNotFound'
    )
  )
  , fileNotFound = function(e){
    cat("caught fileNotFound", fill=T)
    e
  }
  , error = function(e){
    cat("caught:", class(e), fill=T)
    ## handle any other error
  }
)
##
str(rv)
caught fileNotFound
> str(rv)
List of 2
 $ message: chr "file not found."
 $ call   : NULL
 - attr(*, "class")= chr [1:3] "fileNotFound" "error" "condition"

Now, translating to R the fileNotFound example we saw in the first part is fairly straightforward :

##
processFile <- function(fileName){
  if( !file.exists(fileName) ){
    fileNotFoundCondition <- errorCondition(
      message = paste("file", fileName, "not found.")
      , fileName = fileName
      , class= 'fileNotFound'
    )
    stop(fileNotFoundCondition)
  }
}
##
fileNames <- c("file0.txt", "file1.txt", "file2.txt")
##
for( fileName in fileNames){
  tryCatch(  
    {
      processFile(fileName)
    }
    ## Catch fileNotFound error
    , fileNotFound = function(e){
      warning(conditionMessage(e))
    }
    ## Catch any other errors
    , error = function(e){
      ## rethrow
      stop(e)
    }
  )
  ## further computations…
}

tryCatch() behaves in a similar way as the try–catch C++ blocks. When an error is signalled, R unwinds the call stack looking for handlers and cleans up along the way. To handle custom conditions, simply use named handlers. One difference with C++ is that R also provide a finally block (like Java ).

As stated in the conditions man page, the errorCondition() function can be used to construct error conditions of a particular class with additional fields specified as the ... argument. It simply creates an object inheriting from the abstract class condition :

errorCondition(
    message = paste("file", fileName, "not found.")
  , fileName = fileName ## additional field
  , class= 'fileNotFound'
)
List of 3
 $ message : chr "file file0.txt not found."
 $ call    : NULL
 $ fileName: chr "file0.txt"
 - attr(*, "class")= chr [1:3] "fileNotFound" "error" "condition"

Using condition other than message, warning or error is actually an —as of writing— undocumented feature of R. I found out about it while reading tryCatch() source code. As it has always been,

« Use The Source, Luke ! »

For the sake of completeness, errors can also be handled by simply returning the error condition and checking the returned value class :

##
v <- tryCatch(
  {
    …
  }
  , error = function(e) e
)
if( inherits(v, 'error') ){
  … ## handle error
}

in which case you can also use the try() function which is a wrapper around the tryCatch() function.

Also, tryCatch() works with signalCondition() 

##
  tryCatch(
  (function(){ signalCondition(errorCondition("whatever", class="whatever"));T})()
  , whatever = function(e){cat("Caught whatever\n");e}
)

Resumption in R

Common Lisp–like error handling is provided by the withCallingHandlers() function.

Both tryCatch() and withCallingHandlers() call the .Internal function .addCondHands() but behave very differently. Therefore, simply replacing tryCatch() by withCallingHandlers() in the fileNotFound example won’t work as intended :

processFileWithSignal <- function(fileName){
  if( !file.exists(fileName) ){
    fileNotFoundCondition <- errorCondition(
      message = paste("file", fileName, "not found.")
      , fileName = fileName
      , class= 'fileNotFound'
    )
    ## This stops the loop no matter what : stop(fileNotFoundCondition)
    signalCondition(fileNotFoundCondition)
  }
}
for( fileName in fileNames){
  withCallingHandlers( 
    {
      processFileWithSignal(fileName)
    }
    , fileNotFound = function(e){
      warning(conditionMessage(e))
    }
    ## this is called anyway
    , error = function(e){
      cat('Error handler:', conditionMessage(e), '\n')
      ## Uncommenting this will cause the script to terminate 
      ## even thought the fileNotFound handler was called
      ## beacause every handlers are called
      # stop(e)
    }
  )
}

When using stop(), the error never gets caught and the loop stops at the first iteration.

Replacing stop() by signalCondition() in processFile() does not help either. The loop still stops at the first iteration because every handlers in the handler stack will be tried and not only the first handler matching the condition. Therefore, the last handler is called anyway thus interrupting the loop. The loop only continues if we modify or remove the error handler so the error is not rethrown thereby losing the ability to catch any other errors (we’ll fix that in the restarts subsection) :

Error handler: file file0.txt not found. 
Error handler: file file1.txt not found. 
Error handler: file file2.txt not found. 
Messages d'avis :
1: Dans (function (e)  : file file0.txt not found.
2: Dans (function (e)  : file file1.txt not found.
3: Dans (function (e)  : file file2.txt not found.

In order to understand what’s going on, here is a simpler loop that optionally either signals or throws a condition at the third iteration :

f <- function(which=c("cond", "err") ){
  which <- match.arg(which)
  for(i in 1:5){
    cat(i, fill=T)
    if(i==3){
      if(which=="cond")signalCondition("have cond") else stop("has error")
      cat("resume\n")
    }
    cat("…\n")
  }

##
withCallingHandlers(
  f()
  , condition = function(cond){
    cat("caught:", paste(class(cond), collapse = ", "), fill=T)
  }
)
1
…
2
…
3
caught: simpleCondition, condition
resume
…
4
…
5
…

##
rv <- withCallingHandlers(
  f("err")
  , condition = function(cond){
    cat("caught:", paste(class(cond), collapse = ", "), fill=T)
  }
)

1
…
2
…
3
caught: simpleError, error, condition
 Erreur dans f("err") : err

When an error is thrown, the loops stops. But when a condition (or an error) is signalled, the loops goes on. As stated before, signalCondition() only signals a condition but does not alter the flow of the program.

Let’s investigate further the behaviour of withCallingHandlers() with a few more examples :

{
  withCallingHandlers( 
    {
      signalCondition('cond0') ## ① 
      #signalCondition(simpleCondition('cond0')) ## ②
      #structure(list(message = 'got cond0', call = NULL), class = c("cond0", "simpleCondition", "condition")) ## ③ 
      #signalCondition(errorCondition('got cond0', class='cond0')) ## ④
    }
    , simpleCondition = function(e) cat('simpleCondition #0\n')
    , cond0 = function(e) cat('cond0 #0\n')
    , simpleCondition = function(e) cat('simpleCondition #1\n')
    , cond0 = function(e) cat('cond0 #1\n')
    , error = function(e){cat('caught error\n')}
  )
  cat('exit\n')
  T
}

① does the same thing as ② and calls every simpleCondition in order :

simpleCondition #0
simpleCondition #1
exit
[1] TRUE

③ does not call any handler 

exit
[1] TRUE

④ calls every cond0 handler in order as well as the error handler :

cond0 #0
cond0 #1
caught error
exit
[1] TRUE

The take away is that, as stated before, every matching handler will be called (including the generic error when signalling an error condition). In addition, you cannot create conditions inheriting the simpleCondition class so it seems.

Here is another example :

withCallingHandlers( 
  {
    {
      signalCondition(errorCondition('File Not Found', class='fileNotFound'))
    }
    T
  }
  , fileNotFound = function(e){
    if( T ){
      warning( conditionMessage(e) ) ## ① 
    }else{
      e$call = sys.calls()[[sys.nframe()-1]]
      warning(e) ## ②
    }
    return(F)
  }
  , error = function(e){
    cat('error:\n')
    (print(e))
    e
  }
) 

In the first case, the error handler only gets called once :

error:
<fileNotFound: File Not Found>
[1] TRUE
Message d'avis :
Dans (function (e)  : File Not Found 

but is called twice in the second case :

error:
<fileNotFound in signalCondition(errorCondition("fileNotFound", class = "fileNotFound")): File Not Found>
error:
<fileNotFound: File Not Found>
[1] TRUE
Message d'avis :
Dans signalCondition(errorCondition("File Not Found", class = "fileNotFound")) :
  fileNotFound 

Debugging Code with Continuation Handlers

withCallingHandlers() can be used for debugging as demonstrated by the following code snippet which simply open R‘s browser thus allowing us to investigate the context of the error condition :

##
withCallingHandlers(
  (function(x, y){
    x / y
  })(x = 1, y = '2')
  , error = function(e){
    browser()
  }
)

Executing the following commands in the browser

## print call stack
sys.calls()
## list objects in the function 
ls.str(sys.frame(which = 2))
## quit browser
Q

results in

## show error
Browse[1]> e
<simpleError in x/y: argument non numérique pour un opérateur binaire>
Browse[1]> ## print call stack
Browse[1]> sys.calls()
[[1]]
withCallingHandlers((function(x, y) {
    x/y
})(x = 1, y = "2"), error = function(e) {
    browser()
})

[[2]]
(function(x, y) {
    x/y
})(x = 1, y = "2")

[[3]]
x / y

[[4]]
h(simpleError(msg, call))

Browse[1]> ## list objects in the function 
Browse[1]> ls.str(sys.frame(which = 2))
x :  num 1
y :  chr "2"
Browse[1]> ## quit browser
Browse[1]> Q

Since withCallingHandlers() does not unwind the call stack, we can interactively print it with sys.calls() and list objects the calling environment. Actually, since we have access the calling frame, we can also modify objects that are bound to it.

For a more elaborate use of withCallingHandlers() for debugging purpose, please refer to the tryLog package.

restarts Handlers

And now we are ready to take a step further by adding restarts which allow to resume execution where the condition was signalled. In order to do that, we need to establish a restart with the withRestarts() function then use invokeRestart() to call it.

From the manual : invokeRestart() transfers control to the point where the specified restart was established and calls the restart’s handler with the arguments Here is an example :

## 
rv <- withCallingHandlers(
  {
    rv <- withRestarts(
      stop("I'm twice as quit now.")
      , resume = function(e,...){
        cat(conditionMessage(e), fill = T)
        cat("You know the score, pal. You're not cop, you're little people !", fill = T)
        e
      }
    )
    cat("No choice, huh ?", fill = T)
    rv
  }
  , error = function(e){
    invokeRestart('resume', e)
  }
)

This prints

I'm twice as quit now.
You know the score, pal. You're not cop, you're little people !
No choice, huh ?

> rv
<simpleError in doWithOneRestart(return(expr), restart): I'm twice as quit now.>  

The error handler calls the resume() restart with the thrown error as an argument. And once the resume() restart has returned, control is transferred back to the original function thus writing “No choice, huh ?” to stdout (which is fortunate because, otherwise, there’ll be no film to begin with). In addition, the rv object now holds the error that was passed as an argument to resume().

Now we can modify the interrupted loop example we saw earlier to resume execution when an error is thrown :

##
f <- function( ){
  for(i in 1:5){
    cat(i, fill=T)
    withRestarts(
      {
        if(i==3){
          stop(errorCondition(paste0("ignorableError at i=", i), class="ignorableError"))
        }
        cat("…\n")
      }
      , skipIgnorableError = function(e){
        cat("skipIgnorableError\n")
        NULL
      }
    )
  }
}
##
withCallingHandlers(
  f()
  , ignorableError = function(cond){
    cat("caught ignorableError:", conditionMessage(cond), fill=T)
    invokeRestart("skipIgnorableError")
  }
)
1
…
2
…
3
caught ignorableError: ignorableError at i=3
skipIgnorableError
4
…
5
…

Another example is the build–in suppresWarning() function :

##
function (expr, classes = "warning") 
{
    withCallingHandlers(
      expr
      , warning = function(w) if (inherits(w, classes)) tryInvokeRestart("muffleWarning")
    )
}

muffleWarning() is defined in the warning() function :

message <- conditionMessage(cond)
call <- conditionCall(cond)
withRestarts({
    .Internal(.signalCondition(cond, message, call))
    .Internal(.dfltWarn(message, call))
}, muffleWarning = function() NULL)

The difference between invokeRestart() and tryInvokeRestart() is that the latter does not fail if the restarts is not found.

message also provides a muffleMessage() restart.

Now let’s add a restart to the fileNotFound example :

for( fileName in fileNames){
  withCallingHandlers(  
    {
      withRestarts(
        {
          processFile(fileName)
          ## further computations…
        }
        , skipFile = function(e){
          cat("Skipping", e$fileName, "file", fill=T)
          NULL
        }
      )
      ## When withRestarts is called we actually end up here
      cat("next\n")
    }
    , fileNotFound = function(e){
      invokeRestart('skipFile', e)
    }
    , error = function(e){
      ## do something about it
    }
  )
}
Skipping file0.txt file
next
Skipping file1.txt file
next
Skipping file2.txt file
next

Now we can see that invokeRestart() actually transfers control to the point where the specified restart was established (and not to the point where the condition was signalled) and calls the restart’s handler with the arguments. Thus resuming the normal flow of the program.

Also, contrary to the processFileWithSignal() example we saw earlier, other established handler are not called since the fileNotFound handler does not return1.

Now, we’re still somehow terminating the computation and not actually restarting the computation. So, we end up with a very complicated way to achieve what tryCatch() does seamlessly. But since we can resume, we can try to fix the condition on the spot and restart the computation without unwinding the stack. And this is what we’ll cover that in the next post of the series.



Cite this blog post
Thomas Soubiran (2024, January 21). Signalling (throwing) and Handling (catching) Custom Conditions (exceptions) in R Ⅱ : R Termination and Resumption Handlers. NUMA. Retrieved June 19, 2024, from https://doi.org/10.58079/vn4a

  1. From the man page : If the handler returns, then the next handler is tried. []

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Search OpenEdition Search

You will be redirected to OpenEdition Search