8 Conditions

Attaching the needed libraries:

library(rlang, warn.conflicts = FALSE)
library(testthat, warn.conflicts = FALSE)

8.1 Signalling conditions (Exercises 8.2.4)


Q1. Write a wrapper around file.remove() that throws an error if the file to be deleted does not exist.

A1. Let’s first create a wrapper function around file.remove() that throws an error if the file to be deleted does not exist.

fileRemove <- function(...) {
  existing_files <- fs::file_exists(...)

  if (!all(existing_files)) {
    stop(
      cat(
        "The following files to be deleted don't exist:",
        names(existing_files[!existing_files]),
        sep = "\n"
      ),
      call. = FALSE
    )
  }

  file.remove(...)
}

Let’s first create a file that we can delete immediately.

fs::file_create("random.R")

The function should fail if there are any other files provided that don’t exist:

fileRemove(c("random.R", "XYZ.csv"))
#> The following files to be deleted don't exist:
#> XYZ.csv
#> Error:

But it does work as expected when the file exists:

fileRemove("random.R")
#> [1] TRUE

Q2. What does the appendLF argument to message() do? How is it related to cat()?

A2. As mentioned in the docs for message(), appendLF argument decides:

should messages given as a character string have a newline appended?

  • If TRUE (default value), a final newline is regarded as part of the message:
foo <- function(appendLF) {
  message("Beetle", appendLF = appendLF)
  message("Juice", appendLF = appendLF)
}

foo(appendLF = TRUE)
#> Beetle
#> Juice
  • If FALSE, messages will be concatenated:
foo <- function(appendLF) {
  message("Beetle", appendLF = appendLF)
  message("Juice", appendLF = appendLF)
}

foo(appendLF = FALSE)
#> BeetleJuice

On the other hand, cat() converts its arguments to character vectors and concatenates them to a single character vector by default:

foo <- function() {
  cat("Beetle")
  cat("Juice")
}

foo()
#> BeetleJuice

In order to get message()-like default behavior for outputs, we can set sep = "\n":

foo <- function() {
  cat("Beetle", sep = "\n")
  cat("Juice", sep = "\n")
}

foo()
#> Beetle
#> Juice

8.2 Handling conditions (Exercises 8.4.5)


Q1. What extra information does the condition generated by abort() contain compared to the condition generated by stop() i.e. what’s the difference between these two objects? Read the help for ?abort to learn more.

catch_cnd(stop("An error"))
catch_cnd(abort("An error"))

A1. Compared to base::stop(), rlang::abort() contains two additional pieces of information:

  • trace: A traceback capturing the sequence of calls that lead to the current function
  • parent: Information about another condition used as a parent to create a chained condition.
library(rlang)

stopInfo <- catch_cnd(stop("An error"))
abortInfo <- catch_cnd(abort("An error"))

str(stopInfo)
#> List of 2
#>  $ message: chr "An error"
#>  $ call   : language force(expr)
#>  - attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

str(abortInfo)
#> List of 5
#>  $ message: chr "An error"
#>  $ trace  :Classes 'rlang_trace', 'rlib_trace', 'tbl' and 'data.frame':  8 obs. of  6 variables:
#>   ..$ call       :List of 8
#>   .. ..$ : language catch_cnd(abort("An error"))
#>   .. ..$ : language eval_bare(rlang::expr(tryCatch(!!!handlers, {     force(expr) ...
#>   .. ..$ : language tryCatch(condition = `<fn>`, {     force(expr) ...
#>   .. ..$ : language tryCatchList(expr, classes, parentenv, handlers)
#>   .. ..$ : language tryCatchOne(expr, names, parentenv, handlers[[1L]])
#>   .. ..$ : language doTryCatch(return(expr), name, parentenv, handler)
#>   .. ..$ : language force(expr)
#>   .. ..$ : language abort("An error")
#>   ..$ parent     : int [1:8] 0 1 1 3 4 5 1 0
#>   ..$ visible    : logi [1:8] FALSE FALSE FALSE FALSE FALSE FALSE ...
#>   ..$ namespace  : chr [1:8] "rlang" "rlang" "base" "base" ...
#>   ..$ scope      : chr [1:8] "::" "::" "::" "local" ...
#>   ..$ error_frame: logi [1:8] FALSE FALSE FALSE FALSE FALSE FALSE ...
#>   ..- attr(*, "version")= int 2
#>  $ parent : NULL
#>  $ rlang  :List of 1
#>   ..$ inherit: logi TRUE
#>  $ call   : NULL
#>  - attr(*, "class")= chr [1:3] "rlang_error" "error" "condition"

Q2. Predict the results of evaluating the following code

show_condition <- function(code) {
  tryCatch(
    error = function(cnd) "error",
    warning = function(cnd) "warning",
    message = function(cnd) "message",
    {
      code
      NULL
    }
  )
}

show_condition(stop("!"))
show_condition(10)
show_condition(warning("?!"))
show_condition({
  10
  message("?")
  warning("?!")
})

A2. Correctly predicted 😉

The first three pieces of code are straightforward:

show_condition <- function(code) {
  tryCatch(
    error = function(cnd) "error",
    warning = function(cnd) "warning",
    message = function(cnd) "message",
    {
      code
      NULL
    }
  )
}

show_condition(stop("!"))
#> [1] "error"
show_condition(10)
#> NULL
show_condition(warning("?!"))
#> [1] "warning"

The last piece of code is the challenging one and it illustrates how tryCatch() works. From its docs:

When several handlers are supplied in a single tryCatch then the first one is considered more recent than the second.

show_condition({
  10
  message("?")
  warning("?!")
})
#> [1] "message"

Q3. Explain the results of running this code:

withCallingHandlers(
  message = function(cnd) message("b"),
  withCallingHandlers(
    message = function(cnd) message("a"),
    message("c")
  )
)
#> b
#> a
#> b
#> c

A3. The surprising part of this output is the b before the last c.

This happens because the inner calling handler doesn’t handle the message, so it bubbles up to the outer calling handler.


Q4. Read the source code for catch_cnd() and explain how it works.

A4. Let’s look at the source code for catch_cnd():

rlang::catch_cnd
#> function (expr, classes = "condition") 
#> {
#>     stopifnot(is_character(classes))
#>     handlers <- rep_named(classes, list(identity))
#>     eval_bare(rlang::expr(tryCatch(!!!handlers, {
#>         force(expr)
#>         return(NULL)
#>     })))
#> }
#> <bytecode: 0x5612c6763c50>
#> <environment: namespace:rlang>

As mentioned in the function docs:

This is a small wrapper around tryCatch() that captures any condition signalled while evaluating its argument.

The classes argument allows a character vector of condition classes to catch, and the complex tidy evaluation generates the necessary condition (if there is any; otherwise NULL).

catch_cnd(10)
#> NULL

catch_cnd(abort(message = "an error", class = "class1"))
#> <error/class1>
#> Error:
#> ! an error
#> ---
#> Backtrace:
#> ▆

Q5. How could you rewrite show_condition() to use a single handler?

A5. The source code for rlang::catch_cond() gives us a clue as to how we can do this.

Conditions also have a class attribute, and we can use it to determine which handler will match the condition.

show_condition2 <- function(code) {
  tryCatch(
    condition = function(cnd) {
      if (inherits(cnd, "error")) {
        return("error")
      }
      if (inherits(cnd, "warning")) {
        return("warning")
      }
      if (inherits(cnd, "message")) {
        return("message")
      }
    },
    {
      code
      NULL
    }
  )
}

Let’s try this new version with the examples used for the original version:

show_condition2(stop("!"))
#> [1] "error"
show_condition2(10)
#> NULL
show_condition2(warning("?!"))
#> [1] "warning"
show_condition2({
  10
  message("?")
  warning("?!")
})
#> [1] "message"

8.3 Custom conditions (Exercises 8.5.4)


Q1. Inside a package, it’s occasionally useful to check that a package is installed before using it. Write a function that checks if a package is installed (with requireNamespace("pkg", quietly = FALSE)) and if not, throws a custom condition that includes the package name in the metadata.

A1. Here is the desired function:

abort_missing_package <- function(pkg) {
  msg <- glue::glue("Problem loading `{pkg}` package, which is missing and must be installed.")

  abort("error_missing_package",
    message = msg,
    pkg = pkg
  )
}

check_if_pkg_installed <- function(pkg) {
  if (!requireNamespace(pkg, quietly = TRUE)) {
    abort_missing_package(pkg)
  }

  TRUE
}

check_if_pkg_installed("xyz123")
#> Error in `abort_missing_package()`:
#> ! Problem loading `xyz123` package, which is missing and must be installed.
check_if_pkg_installed("dplyr")
#> [1] TRUE

For a reference, also see the source code for following functions:


Q2. Inside a package you often need to stop with an error when something is not right. Other packages that depend on your package might be tempted to check these errors in their unit tests. How could you help these packages to avoid relying on the error message which is part of the user interface rather than the API and might change without notice?

A2. As an example, let’s say that another package developer wanted to use the check_if_pkg_installed() function that we just wrote.

So the developer using it in their own package can write a unit test like this:

expect_error(
  check_if_pkg_installed("xyz123"),
  "Problem loading `xyz123` package, which is missing and must be installed."
)

To dissuade developers from having to rely on error messages to check for errors, we can instead provide a custom condition, which can be used for unit testing instead:

e <- catch_cnd(check_if_pkg_installed("xyz123"))

inherits(e, "error_missing_package")
#> [1] TRUE

So that the unit test could be:

expect_s3_class(e, "error_missing_package")

This test wouldn’t fail even if we decided to change the exact message.


8.4 Applications (Exercises 8.6.6)


Q1. Create suppressConditions() that works like suppressMessages() and suppressWarnings() but suppresses everything. Think carefully about how you should handle errors.

A1. To create the desired suppressConditions(), we just need to create an equivalent of suppressWarnings() and suppressMessages() for errors. To suppress the error message, we can handle errors within a tryCatch() and return the error object invisibly:

suppressErrors <- function(expr) {
  tryCatch(
    error = function(cnd) invisible(cnd),
    expr
  )
}

suppressConditions <- function(expr) {
  suppressErrors(suppressWarnings(suppressMessages(expr)))
}

Let’s try out and see if this works as expected:

suppressConditions(1)
#> [1] 1

suppressConditions({
  message("I'm messaging you")
  warning("I'm warning you")
})

suppressConditions({
  stop("I'm stopping this")
})

All condition messages are now suppressed, but note that if we assign error object to a variable, we can still extract useful information for debugging:

e <- suppressConditions({
  stop("I'm stopping this")
})

e
#> <simpleError in withCallingHandlers(expr, message = function(c) if (inherits(c,     classes)) tryInvokeRestart("muffleMessage")): I'm stopping this>

Q2. Compare the following two implementations of message2error(). What is the main advantage of withCallingHandlers() in this scenario? (Hint: look carefully at the traceback.)

message2error <- function(code) {
  withCallingHandlers(code, message = function(e) stop(e))
}
message2error <- function(code) {
  tryCatch(code, message = function(e) stop(e))
}

A2. With withCallingHandlers(), the condition handler is called from the signaling function itself, and, therefore, provides a more detailed call stack.

message2error1 <- function(code) {
  withCallingHandlers(code, message = function(e) stop("error"))
}

message2error1({
  1
  message("hidden error")
  NULL
})
#> Error in (function (e) : error

traceback()
#> 9: stop("error") at #2
#> 8: (function (e)
#>    stop("error"))(list(message = "hidden error\n",
#>      call = message("hidden error")))
#> 7: signalCondition(cond)
#> 6: doWithOneRestart(return(expr), restart)
#> 5: withOneRestart(expr, restarts[[1L]])
#> 4: withRestarts({
#>        signalCondition(cond)
#>        defaultHandler(cond)
#>    }, muffleMessage = function() NULL)
#> 3: message("hidden error") at #1
#> 2: withCallingHandlers(code,
#>      message = function(e) stop("error")) at #2
#> 1: message2error1({
#>        1
#>        message("hidden error")
#>        NULL
#>    })

With tryCatch(), the signalling function terminates when a condition is raised, and so it doesn’t provide as detailed call stack.

message2error2 <- function(code) {
  tryCatch(code, message = function(e) (stop("error")))
}

message2error2({
  1
  stop("hidden error")
  NULL
})
#> Error in value[[3L]](cond) : error

traceback()
#> 6: stop("error") at #2
#> 5: value[[3L]](cond)
#> 4: tryCatchOne(expr, names, parentenv, handlers[[1L]])
#> 3: tryCatchList(expr, classes, parentenv, handlers)
#> 2: tryCatch(code, message = function(e) (stop("error"))) at #2
#> 1: message2error2({
#>        1
#>        message("hidden error")
#>        NULL
#>    })

Q3. How would you modify the catch_cnds() definition if you wanted to recreate the original intermingling of warnings and messages?

A3. Actually, you won’t have to modify anything about the function defined in the chapter, since it supports this out of the box.

So nothing additional to do here5! 😅

catch_cnds <- function(expr) {
  conds <- list()
  add_cond <- function(cnd) {
    conds <<- append(conds, list(cnd))
    cnd_muffle(cnd)
  }

  withCallingHandlers(
    message = add_cond,
    warning = add_cond,
    expr
  )

  conds
}

catch_cnds({
  inform("a")
  warn("b")
  inform("c")
})
#> [[1]]
#> <message/rlang_message>
#> Message:
#> a
#> 
#> [[2]]
#> <warning/rlang_warning>
#> Warning:
#> b
#> 
#> [[3]]
#> <message/rlang_message>
#> Message:
#> c

Q4. Why is catching interrupts dangerous? Run this code to find out.

bottles_of_beer <- function(i = 99) {
  message(
    "There are ", i, " bottles of beer on the wall, ",
    i, " bottles of beer."
  )
  while (i > 0) {
    tryCatch(
      Sys.sleep(1),
      interrupt = function(err) {
        i <<- i - 1
        if (i > 0) {
          message(
            "Take one down, pass it around, ", i,
            " bottle", if (i > 1) "s", " of beer on the wall."
          )
        }
      }
    )
  }
  message(
    "No more bottles of beer on the wall, ",
    "no more bottles of beer."
  )
}

A4. Because this function catches the interrupt and there is no way to stop bottles_of_beer(), because the way you would usually stop it by using interrupt!

bottles_of_beer()
#> There are 99 bottles of beer on the wall, 99 bottles of beer.
#> Take one down, pass it around, 98 bottles of beer on the wall.
#> Take one down, pass it around, 97 bottles of beer on the wall.
#> Take one down, pass it around, 96 bottles of beer on the wall.
#> Take one down, pass it around, 95 bottles of beer on the wall.
#> Take one down, pass it around, 94 bottles of beer on the wall.
#> Take one down, pass it around, 93 bottles of beer on the wall.
#> Take one down, pass it around, 92 bottles of beer on the wall.
#> Take one down, pass it around, 91 bottles of beer on the wall.
#> ...

In RStudio IDE, you can snap out of this loop by terminating the R session.

This shows why catching interrupt is dangerous and can result in poor user experience.


8.5 Session information

sessioninfo::session_info(include_base = TRUE)
#> ─ Session info ───────────────────────────────────────────
#>  setting  value
#>  version  R version 4.4.0 (2024-04-24)
#>  os       Ubuntu 22.04.4 LTS
#>  system   x86_64, linux-gnu
#>  ui       X11
#>  language (EN)
#>  collate  C.UTF-8
#>  ctype    C.UTF-8
#>  tz       UTC
#>  date     2024-05-20
#>  pandoc   3.2 @ /opt/hostedtoolcache/pandoc/3.2/x64/ (via rmarkdown)
#> 
#> ─ Packages ───────────────────────────────────────────────
#>  package     * version date (UTC) lib source
#>  base        * 4.4.0   2024-05-06 [3] local
#>  bookdown      0.39    2024-04-15 [1] RSPM
#>  brio          1.1.5   2024-04-24 [1] RSPM
#>  bslib         0.7.0   2024-03-29 [1] RSPM
#>  cachem        1.1.0   2024-05-16 [1] RSPM
#>  cli           3.6.2   2023-12-11 [1] RSPM
#>  compiler      4.4.0   2024-05-06 [3] local
#>  datasets    * 4.4.0   2024-05-06 [3] local
#>  desc          1.4.3   2023-12-10 [1] RSPM
#>  digest        0.6.35  2024-03-11 [1] RSPM
#>  downlit       0.4.3   2023-06-29 [1] RSPM
#>  dplyr         1.1.4   2023-11-17 [1] RSPM
#>  evaluate      0.23    2023-11-01 [1] RSPM
#>  fansi         1.0.6   2023-12-08 [1] RSPM
#>  fastmap       1.2.0   2024-05-15 [1] RSPM
#>  fs            1.6.4   2024-04-25 [1] RSPM
#>  generics      0.1.3   2022-07-05 [1] RSPM
#>  glue          1.7.0   2024-01-09 [1] RSPM
#>  graphics    * 4.4.0   2024-05-06 [3] local
#>  grDevices   * 4.4.0   2024-05-06 [3] local
#>  htmltools     0.5.8.1 2024-04-04 [1] RSPM
#>  jquerylib     0.1.4   2021-04-26 [1] RSPM
#>  jsonlite      1.8.8   2023-12-04 [1] RSPM
#>  knitr         1.46    2024-04-06 [1] RSPM
#>  lifecycle     1.0.4   2023-11-07 [1] RSPM
#>  magrittr    * 2.0.3   2022-03-30 [1] RSPM
#>  memoise       2.0.1   2021-11-26 [1] RSPM
#>  methods     * 4.4.0   2024-05-06 [3] local
#>  pillar        1.9.0   2023-03-22 [1] RSPM
#>  pkgconfig     2.0.3   2019-09-22 [1] RSPM
#>  pkgload       1.3.4   2024-01-16 [1] RSPM
#>  R6            2.5.1   2021-08-19 [1] RSPM
#>  rlang       * 1.1.3   2024-01-10 [1] RSPM
#>  rmarkdown     2.27    2024-05-17 [1] RSPM
#>  rprojroot     2.0.4   2023-11-05 [1] RSPM
#>  sass          0.4.9   2024-03-15 [1] RSPM
#>  sessioninfo   1.2.2   2021-12-06 [1] RSPM
#>  stats       * 4.4.0   2024-05-06 [3] local
#>  testthat    * 3.2.1.1 2024-04-14 [1] RSPM
#>  tibble        3.2.1   2023-03-20 [1] RSPM
#>  tidyselect    1.2.1   2024-03-11 [1] RSPM
#>  tools         4.4.0   2024-05-06 [3] local
#>  utf8          1.2.4   2023-10-22 [1] RSPM
#>  utils       * 4.4.0   2024-05-06 [3] local
#>  vctrs         0.6.5   2023-12-01 [1] RSPM
#>  withr         3.0.0   2024-01-16 [1] RSPM
#>  xfun          0.44    2024-05-15 [1] RSPM
#>  xml2          1.3.6   2023-12-04 [1] RSPM
#>  yaml          2.3.8   2023-12-11 [1] RSPM
#> 
#>  [1] /home/runner/work/_temp/Library
#>  [2] /opt/R/4.4.0/lib/R/site-library
#>  [3] /opt/R/4.4.0/lib/R/library
#> 
#> ──────────────────────────────────────────────────────────