8.4 Handling conditions

Every condition has a default behavior:

  • ❌ Errors halt execution
  • ⚠️ Warnings are collected during execution and displayed in bulk after execution
  • 💬 Messages are displayed immediately

Condition handlers allow one to change that behavior (within the scope of a function).

Two handler functions:

  • tryCatch()
  • withCallingHandlers()
# try to run `code_to_try_to_run`
# if (error) condition is signalled, fun some other code
tryCatch(
  error = function(cnd) {
    # code to run when error is thrown
  },
  code_to_try_to_run
)

# try to `code_to_try_to_run`
# if condition is signalled, run code corresponding to condition type
withCallingHandlers(
  warning = function(cnd) {
    # code to run when warning is signalled
  },
  message = function(cnd) {
    # code to run when message is signalled
  },
  code_to_try_to_run
)

8.4.1 Condition objects

# catch a condition
cnd <- rlang::catch_cnd(stop("An error"))
# inspect it
str(cnd)
#> List of 2
#>  $ message: chr "An error"
#>  $ call   : language force(expr)
#>  - attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

The standard components

  • message. The error message. To extract it, use conditionMessage(cnd).
  • call. The function call that triggered the condition. To extract it, use conditionCall(cnd).

But custom conditions may contain other components.

8.4.2 Exiting handlers

If a condition is signalled, this type of handler controls what code to run before exiting the function call.

f3 <- function(x) {
  tryCatch(
    # if error signalled, return NA
    error = function(cnd) NA,
    # try to run log
    log(x)
  )
}

f3("x")
#> [1] NA

When a condition is signalled, control moves to the handler and never returns to the original code.

tryCatch(
  message = function(cnd) "There",
  {
    message("Here")
    stop("This code is never run!")
  }
)
#> [1] "There"

The tryCatch() exit handler has one final argument: finally. This is run regardless of the condition of the original code. This is often used for clean-up.

# try to write text to disk
# if an error is signalled--for example, `path` does not exist
# or if no condition is signalled
# that is in both cases, the code block in `finally` is executed
path <- tempfile()
tryCatch(
  {
    writeLines("Hi!", path)
    # ...
  },
  finally = {
    # always run
    unlink(path)
  }
)

8.4.3 Calling handlers

Definition by verbal comparison:

  • With exit handlers, code exits the normal flow once a condition is signalled
  • With calling handlers, code continues in the normal flow once control is returned by the handler.

Definition by code comparison:

# with an exit handler, control moves to the handler once condition signalled and does not move back
tryCatch(
  message = function(cnd) cat("Caught a message!\n"), 
  {
    message("Someone there?")
    message("Why, yes!")
  }
)
#> Caught a message!

# with a calling handler, control moves first to the handler and the moves back to the main code
withCallingHandlers(
  message = function(cnd) cat("Caught a message!\n"), 
  {
    message("Someone there?")
    message("Why, yes!")
  }
)
#> Caught a message!
#> Someone there?
#> Caught a message!
#> Why, yes!

8.4.4 By default, conditions propagate

Let’s suppose that there are nested handlers. If a condition is signalled in the child, it propagates to its parent handler(s).

# Bubbles all the way up to default handler which generates the message
withCallingHandlers(
  message = function(cnd) cat("Level 2\n"),
  withCallingHandlers(
    message = function(cnd) cat("Level 1\n"),
    message("Hello")
  )
)
#> Level 1
#> Level 2
#> Hello

# Bubbles up to tryCatch
tryCatch(
  message = function(cnd) cat("Level 2\n"),
  withCallingHandlers(
    message = function(cnd) cat("Level 1\n"),
    message("Hello")
  )
)
#> Level 1
#> Level 2

8.4.5 But conditions can be muffled

If one wants to “muffle” the siginal, one needs to use rlang::cnd_muffle()

# Muffles the default handler which prints the messages
withCallingHandlers(
  message = function(cnd) {
    cat("Level 2\n")
    rlang::cnd_muffle(cnd)
  },
  withCallingHandlers(
    message = function(cnd) cat("Level 1\n"),
    message("Hello")
  )
)
#> Level 1
#> Level 2

# Muffles level 2 handler and the default handler
withCallingHandlers(
  message = function(cnd) cat("Level 2\n"),
  withCallingHandlers(
    message = function(cnd) {
      cat("Level 1\n")
      rlang::cnd_muffle(cnd)
    },
    message("Hello")
  )
)
#> Level 1

8.4.6 Call stacks

Call stacks of exiting and calling handlers differ.

Why?

Calling handlers are called in the context of the call that signalled the condition exiting handlers are called in the context of the call to tryCatch()

To see this, consider how the call stacks differ for a toy example.

# create a function
f <- function() g()
g <- function() h()
h <- function() message

# call stack of calling handlers
withCallingHandlers(f(), message = function(cnd) {
  lobstr::cst()
  rlang::cnd_muffle(cnd)
})
#> function (..., domain = NULL, appendLF = TRUE) 
#> {
#>     cond <- if (...length() == 1L && inherits(..1, "condition")) {
#>         if (nargs() > 1L) 
#>             warning("additional arguments ignored in message()")
#>         ..1
#>     }
#>     else {
#>         msg <- .makeMessage(..., domain = domain, appendLF = appendLF)
#>         call <- sys.call()
#>         simpleMessage(msg, call)
#>     }
#>     defaultHandler <- function(c) {
#>         cat(conditionMessage(c), file = stderr(), sep = "")
#>     }
#>     withRestarts({
#>         signalCondition(cond)
#>         defaultHandler(cond)
#>     }, muffleMessage = function() NULL)
#>     invisible()
#> }
#> <bytecode: 0x560e4fb13780>
#> <environment: namespace:base>

# call stack of exit handlers
tryCatch(f(), message = function(cnd) lobstr::cst())
#> function (..., domain = NULL, appendLF = TRUE) 
#> {
#>     cond <- if (...length() == 1L && inherits(..1, "condition")) {
#>         if (nargs() > 1L) 
#>             warning("additional arguments ignored in message()")
#>         ..1
#>     }
#>     else {
#>         msg <- .makeMessage(..., domain = domain, appendLF = appendLF)
#>         call <- sys.call()
#>         simpleMessage(msg, call)
#>     }
#>     defaultHandler <- function(c) {
#>         cat(conditionMessage(c), file = stderr(), sep = "")
#>     }
#>     withRestarts({
#>         signalCondition(cond)
#>         defaultHandler(cond)
#>     }, muffleMessage = function() NULL)
#>     invisible()
#> }
#> <bytecode: 0x560e4fb13780>
#> <environment: namespace:base>
tryCatch(f(), message = function(cnd) lobstr::cst())
#> function (..., domain = NULL, appendLF = TRUE) 
#> {
#>     cond <- if (...length() == 1L && inherits(..1, "condition")) {
#>         if (nargs() > 1L) 
#>             warning("additional arguments ignored in message()")
#>         ..1
#>     }
#>     else {
#>         msg <- .makeMessage(..., domain = domain, appendLF = appendLF)
#>         call <- sys.call()
#>         simpleMessage(msg, call)
#>     }
#>     defaultHandler <- function(c) {
#>         cat(conditionMessage(c), file = stderr(), sep = "")
#>     }
#>     withRestarts({
#>         signalCondition(cond)
#>         defaultHandler(cond)
#>     }, muffleMessage = function() NULL)
#>     invisible()
#> }
#> <bytecode: 0x560e4fb13780>
#> <environment: namespace:base>