Error handling in R with tryCatchLog: Catching, logging, post-mortem analysis

Jürgen Altfeld

2021-10-24

For a slide version of this vignette (e. g. for trainings and presentations) see:

https://aryoda.github.io/tutorials/tryCatchLog/tryCatchLog-intro-slides.html

License (GPL-3)

gplv3 logo

gplv3 logo

Copyright (C) 2016++ Jürgen Altfeld ()

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

Overview

The tryCatchLog package provides an advanced tryCatch function for the programming language R.

The main advantages of the tryCatchLog function over tryCatch are:

Table of contents

Part 1: tryCatch in standard R

Introduction into conditions in standard R Throw your own conditions Handling conditions in R The drawbacks of tryCatch Workaround 1: Interactive debugging Workaround 2: withCallingHandlers + tryCatch

Part 2: Package tryCatchLog

Better error handling with the tryCatchLog package Post-mortem analysis tryCatchLog Function Reference tryCatchLog Best Practices

Appendix

Introduction into conditions in standard R

What is a condition?

The execution of an R script can be interrupted to signal special states (conditions) like:

For details see the R help: ?conditions

Condition examples

An error condition:

## Error in log("text"): non-numeric argument to mathematical function

A warning condition:

## Warning in log(-1): NaNs produced
## [1] NaN

Throw your own conditions

Note: This text will never be shown due to a “limitation by design” of pandoc:

https://stackoverflow.com/a/31778080/4468078

Throw an Error

Use stop to throw an error “condition” to signal an invalid program state:

## Error in eval(expr, envir, enclos): something is wrong


Or shorter (but without a way to specify an error text):

## Error: 1 == 2 is not TRUE

stopifnot is quite often used to ensure pre-conditions in function calls.

Throw a Warning

## Warning: bad weather today, don't forget your umbrella

Throw a Message

## good morning

Handling conditions in R

Scroll down for examples…

Unhandled errors stop R

By default R will stop the execution if an error occurs:

## Error in log("not a number"): non-numeric argument to mathematical function

Note that the output does not show the print result since the execution stops in case of an error.

Use try to ignore errors

With the try function you can handle errors to continue the execution (by ignoring the error):

## [1] "errors can't stop me"

Note: If an error occurs then the error message is printed to the stderr connection unless the call includes silent = TRUE.

Use tryCatch to handle errors

With tryCatch you can handle errors as you want:

## [1] TRUE


If you use an error handler function

tryCatch can handle all conditions 1/2

… not only errors. Just use the condition name as parameter to handle conditions of this type, e. g. warnings:

## [1] "Hey, a warning"

tryCatch can handle all conditions 2/2

You can use error, warning, message or interrupt as parameter name to assign a handler for these “standard” conditions, e. g. to catch messages:

## [1] "please handle me\n"

Advanced topic: User defined conditions

You can even define your own user-defined condition classes, but there is no built-in function to generate a new object of class condition. So do it yourself:*

## [1] "after work party"

User-defined condition classes are only required if you want to implement a specific recovery strategy for this condition. This is out of scope of this presentation.

*) Source: http://adv-r.had.co.nz/beyond-exception-handling.html

But tryCatch is not perfect

Have you discovered the problem in the previous examples?

See the next chapter for the answer!

The drawbacks of tryCatch

Handling a condition stops the execution 1/2

Calling the function without a tryCatch handler does not stop the execution of the function f():

## Warning in f(): deprecated function called
## [1] "Hello world"
## [1] "Done"

but…

Handling a condition stops the execution 2/2

Handling a condition cancels the execution of the code block that raised (throwed) the condition:

## [1] "Done"

Observe: Hello world is never printed just because we catched a warning!

If you catch a condition with tryCatch (even just a warning or message) then R

  1. executes the condition handler function
  2. aborts the execution of the code block that throwed the condition
  3. continues the execution with the next command after the tryCatch command

Handling errors unrolls the stack trace 1/2

If you do not catch an error R stops and you can get the complete function call stack using traceback to identify the code that throwed the error:

Note: The call stack shows the line number after the file name and hash sign, e. g. file1.R#7 = line number 7

Handling errors unrolls the stack trace 2/2

But if you handle the error, the call stack is truncated:

The call stack ends basically with the tryCatch call but does not show you the code line in f() where the error was thrown.

Summary: The drawbacks of tryCatch

  1. You can not find out the exact reason for errors because the full stack trace is truncated

  2. Handling of warnings and messages (e. g. just to log them) cancels the execution of the code block that throwed the condition (what is unexpected!)

See the next chapters for possible work-arounds…

Workaround 1: Interactive debugging

Interactive debugging

You can run and debug your R script interactively in the RGui or RStudio instead of condition handling with tryCatch.

For more details on interactive debugging see ?debug.

Note: Interactive debugging is out of scope of this presentation.

Limitations of interactive debugging

Interactive debugging is very difficult in case of

Workaround 2: withCallingHandlers + tryCatch

How withCallingHandlers works

withCallingHandlers works similar to tryCatch but

  1. remembers the call stack down to the point where the condition was signaled
  2. resumes the execution after the point where the condition was signaled
## Warning in f(): deprecated function called
## [1] "Hello world"

Note: Use sys.calls within withCallingHandlers to return the full call stack.

withCallingHandlers supports restarts

Restarts allow to recover from conditions using a predefined behaviour:

## [1] "Hello old world"
## [1] "Done"

invokeRestart("muffleWarning") has a simple recovery strategy: “Suppress the warning”.

It consumes the warning (so it does not “bubble up” to higher function call levels) and resumes the execution.

TODO: Mention other restarts and their behaviour…

Differences between withCallingHandlers and tryCatch

tryCatch withCallingHandlers
Program execution breaks and continues with the first expression after the tryCatch function call resumes the execution at the code line that throwed the condition
Call stack (traceback and sys.calls) unwinds the call stack up to the tryCatch function call keeps the full call stack down to the code line that throwed the condition
Rethrowing of conditions Conditions are consumed by the called handler function (do not bubble up) Conditions bubble up (are not consumed by the called handler function)

Note: tryCatch is different from Java’s try-catch statement: It unwinds the call stack (in Java you get the full call stack with the printStackTrace method)!

Combine withCallingHandlers with tryCatch

The requirements for better condition handling in R are:

Solution:

Code snippet for better error handling

An improved “error handler” in R looks similar to this code snippet:

## [1] "A warning cannot stop me"
## [1] "recovered from error"
## [1] "Done"

This is basically how the tryCatchLog package works internally!

How about usability?

Do you really want to use that much boilerplate code in your R scripts at every place where you have to catch errors and conditions?

If not: See the the next chapter to learn how the package tryCatchLog could make your life much easier!

Better error handling with the tryCatchLog package

Installation

To install the package tryCatchLog from the source code use:

For more details see the Project site at: https://github.com/aryoda/tryCatchLog

Overview

The tryCatchLog package improves the standard R’s try and tryCatch functions by offering extended functions:

Condition handling strategy Standard R tryCatchLog package
Return an error object in case of errors try() tryLog()
Call condition handler functions tryCatch() tryCatchLog()

Improvements:

  1. Configurable logging (“for free”)
  2. Logging of full or compact call stack with line numbers
  3. Resume after warnings and messages
  4. Support for post-mortem analysis after errors via dump files

tryLog example with an error

Errors are logged but the execution continues after the tryLog call:

tryLog example with a warning

tryLog catches conditions and logs them onto console or into a file (depending of the settings of the logging framework futile.logger that is used internally):

tryCatchLog example to log and recover from an error

Use tryCatchLog to establish an error handler:

The console shows the log output then and the execution continues:

Note: send.email is a dummy function for demonstration purposes!

How to change the logging behaviour

To log to a file instead of the console or to change the logging level you call the usual futile.logger functions:

For more details about futile.logger see:

https://cran.r-project.org/package=futile.logger

Post-mortem analysis

Known limitations of interactive debugging in R

Interactive debugging using an IDE or the console is very difficult in case of

Solution: Post-mortem analysis

Post-mortem analysis means to create a dump file in case of an error that contains

so that you can

to find out the reason for the error.

Enable post-mortem analysis

tryCatchLog supports post-mortem analysis by creating dump files in case of errors:

Start a post-mortem analysis

Open a new R session and start the post-mortem analysis of the error:

The function call #13 shows: The error was thrown in the file test.R at line #3: log(value)

Post-mortem debugging: Examine an environment

Switch into the environment of the function call #12 which called the function that throwed the error and examine the objects visible within this function:

By looking at the (function argument) variable value it is easy to identify the reason for the error: The passed value “100” had the wrong data type!

You can exit the debugger now with “Q” (or “f” followed by “0”) and fix the bug.

Post-mortem debugging: Limitations

R dump files (created with save.image) do not contain the loaded packages when the dump file was created.

Therefore a dump loaded into memory later does not load these packages automatically.

This means the program state as of the error is not exactly reproducible:

For more details see: https://github.com/aryoda/tryCatchLog/issues/12

tryCatchLog Function Reference

tryCatchLog()

Function signature:

This function evaluates the expression in expr and passes all condition handlers in ... to tryCatch as-is while error, warning and message conditions are logged together with the function call stack (including file names and line numbers).

The expression in finally is always evaluated at the end.

Warnings and messages can be “silenced” (only logged but not propagated to the caller) using the silent.* parameters.

The default values of some parameters can be set globally via options to avoid passing the same parameter values in each call and to support easy reconfiguration for all calls without changing the code.

tryLog()

Function signature:

This function is a short version of tryCatchLog() that traps any errors that occur during the evaluation of the expression expr without stopping the execution of the script (similar to try in R). Errors, warnings and messages are logged.

In contrast to tryCatchLog() it returns an object of the class “try-error” in case of an error and continues after the tryLog expression. Therefore tryLog does not support the error and finally parameters for passing custom handler functions.

The default values of some parameters can be set globally via options to avoid passing the same parameter values in each call and to support easy reconfiguration for all calls without changing the code.

Change global options of tryCatchLog

The default values of many options can be changed globally by configuring them once to reduce lengthy function calls later and support easy reconfiguration for all calls without changing the code:

tryCatchLog Best Practices

Easiest way to add logging of all conditions

Just wrap the call to the main function or main script with tryCatchLog():

Enabling source code references (file names and line numbers)

To show file names and line numbers in the stack trace of the log output:

FAQ

You can find a FAQ with best practices at:

https://github.com/aryoda/tryCatchLog#faq

Appendix

References

Documentation of the futile.logger logging framework: https://github.com/zatonovo/futile.logger

Download of these slides: https://github.com/aryoda/R_trainings

Project home of the tryCatchLog package: https://github.com/aryoda/tryCatchLog

https://www.biostat.jhsph.edu/~rpeng/docs/R-debug-tools.pdf

https://journal.r-project.org/archive/2010-2/RJournal_2010-2_Murdoch.pdf