Introducing Catchr 0.2.0 🎣: Learnable Condition-handling in R

I guess I never made a post about it, but a while ago I had my first R package accepted to CRAN: catchr 0.1.0. It started based on a function I had written that I found myself using time and time again: a function that would collect all the warnings, messages, and errors raised in the process of evaluating code, and return them with the result of the code.

I was so proud of this “clever” code, but something eventually happened that made me take a deep dive into the rlang package by the RStudio people. Their code was so much better than mine that I felt viscerally humiliated.

After recovering a little from the shock, I reevaluated all of my preconceived notions for the package, and outlined the tools I wanted to provide people with. From there, I basically rebuilt it from scratch in a principled way, and turned it into something I’m actually proud of.

I present to you, catchr 0.2.0!

“Exceptions?” “Handlers?” Making sense of conditions in R

Compared to many other programming languages, the way R handles ‘conditions’—errors, warnings, messages, and most things referred to as ‘exceptions’ in other languages—is pretty unintuitive,1 and can often be troublesome to users coming from different backgrounds. For example, on the surface the way exceptions are caught in Python seems so simple compared to R—_what even is a “restart”? What are those things people are referring to as “handlers” anyway?_ R’s help file on the subject is basically impenetrable,2 and even though Hadley Wickham has stepped in and produced a very helpful guide on conditions, understanding what’s going on is still pretty hard, and actually handling conditions is even harder.

At its heart, catchr is designed to make it so that you never have to read any of that stuff make dealing with conditions easy from the get-go.

The purpose of catchr is to provide flexible, useful tools for handling R conditions with less hassle and head-scratching. One of the most important goals of this package is to maintain a continuous learning curve that so new users jump straight in, but more advanced users can find the depth and complexity they need to take advantage of R’s powerful condition-handling abilities.

To lower the barrier of entry, keep code clean and readable, and reduce the amount of typing required, catchr uses a very simple domain-specific language that simplifies things on the front-end. catchr focuses on letting users build their own “catching” functions where they can specify behavior via conceptual “plans”, removing unnecessary complexities—like the distinction between “calling” vs. “exiting” handlers—and adding many useful features, like the ability to “collect” the conditions raised from a call.

The ol’ razzle-dazzle

Let me hit you with a few scenarios here, to help you get a sense of what catchr can do. For more details, please check out the manual and help docs, which I stuffed with information. Suppose I want to run some code, and I want to catch any warnings it produces, “hide” them so they don’t appear, and instead just store them so I can do something else with them later on. All without making my code halt or start over.

library(catchr)
results <- catch_expr(arbitrary_code(), warning = c(collect, muffle))

Boom, done.

But if I’m going to need to do that a lot, and I’m good programmer who doesn’t want to retype the same code over and over again, I can make a function that can catch conditions the same way for any expression.

collect_warnings <- make_catch_fn(warning = c(collect, muffle))

One line. Look at my nice, portable function:

same_results <- collect_warnings(arbitrary_code())

But let’s say I’m a naaaaasty boy3 I have a complex situation which necessitates printing something when the code raises a message and immediately exiting it, but also turning any warnings into messages, and preventing any errors from crashing everything else. *Shiver*.

weirdo_catcher <- make_catch_fn(
  message = c(function(x) print("oops"), exit),
  warning = c(tomessage, muffle),
  error = muffle)

I can do that too, and that’s only beginning to scratch the surface of catchr. Although most of the detailed information can be found in the help files and manual, let’s go over some of the basics.

Catching conditions with “plans”

Let’s take a look at some of the code we saw before:

results <- catch_expr(arbitrary_code(), warning = c(collect, muffle))

This is pretty quintessential of the catchr system. You have the expression you want to evaluate and catch conditions for (here, arbitrary_code()), followed by the names of the types of conditions you want to catch and the plans you have for them when they’re caught (warning = c(collect, muffle)).

In catchr, users use functions like building blocks to a “plan” of what to do for particular conditions. Users can specify their own functions or use catchr functions, but catcher also offers a useful toolbox of behaviors that work their magic behind the scene through catchr’s simple domain-specific language.4

A catchr “plan” starts off as a named argument, where its name is the type of condition it will apply to (e.g., “message”, “warning”, “error”, etc.), and its value will eventually become a function that will take in the condition and do stuff with it. catchr’s “secret sauce” lets you pass in each plan as a list of functions that will be applied to the condition in order.5

But catchr also comes full of stock functions that make condition handling simple—these special functions can be inputted as strings, but catchr promotes more-readable code and saves you extra typing by letting you enter them as unquoted special terms, like collect and muffle in the example above.

Special reserved terms

These special terms cover some of the most common behaviors users might want to use:

Special “reserved” term functionality
tomessage, towarning, toerror convert conditions to other types of conditions
beep plays a short sound
display displays the contents of the condition on-screen
collect collects the condition and saves it to a list that will be returned at the end
muffle “muffles”,6 a condition so it doesn’t keep going up, and restarts the code
exit immediately stops the code and muffles the condition
raise raises conditions past exit

These can be used as building blocks just like normal functions. For example, in the previous example, we saw how collect and muffle were strung together to make a plan.

Even less typing

Is typing out plans multiple times still too much typing? Well, catchr lets you save even more space by giving you the option of passing in unnamed arguments into plans!

print(get_default_plan())
## [[1]]
## [1] "collect"
## 
## [[2]]
## [1] "muffle"
my_plans <- make_plans(warning, message, error)

These unnamed arguments can be entered as either strings or unquoted terms, and should correspond to the name of a condition you want to catch. They will automatically be given whatever catchr’s default plan is, which can be get/set via get_default_plan() and set_default_plan, respectively. Named and unnamed arguments can be mixed freely. Other default behaviors can be get/set with catchr_default_opts().

“Collecting” conditions

As you might have noticed, many of the previous examples use a special term, collect. Having the ability to “collect” conditions is one of the most useful features in catchr.

throw_a_fit <- function() {
  message("This is message #1!")
  rlang::warn("This is warning #1!", 
              opt_val = "conditions can hold more data")
  message("This is message #2")
  stop("Code exits after this!")
  warning("This warning won't be reached")
}

collected_results <- catch_expr(throw_a_fit(), message, warning, error)
print(collected_results)
## $value
## NULL
## 
## $message
## $message[[1]]
## <simpleMessage in message("This is message #1!"): This is message #1!
## >
## 
## $message[[2]]
## <simpleMessage in message("This is message #2"): This is message #2
## >
## 
## 
## $warning
## $warning[[1]]
## <warning: This is warning #1!>
## 
## 
## $error
## $error[[1]]
## <simpleError in throw_a_fit(): Code exits after this!>

This is particularly useful if you want to catch warnings and messages, etc. from code that takes a long time to run, where having to restart the whole process from square one would be too costly.

Or if you want to collect warnings, messages, and errors from code that is running remotely, where these conditions would not be returned with the rest of the results, such as with the future package. The following isn’t a very… natural example, but I’m trying to keep things simple:

library(future)
future::plan(multiprocess)

possible_scenarios <- list(
  quote(warning("Model failed to converge!")),
  quote(message("Singular fit!")),
  quote(stop("You couldn't something wrong")),
  quote("Everything is good!")
)

collector <- make_catch_fn(
  message, warning, error,
  .opts = catchr_opts(default_plan = c(collect, muffle)))

l %<-% {
  # You should use `purrr::map` instead, it's much better
  Map(function(x) collector(eval(x)), possible_scenarios)
}

# Eh, let's get rid of anything that had an error?
Filter(function(e) length(e$error) == 0, l)

OR, maybe you’re running a lot of code in parallel and want to keep track of all of the raised conditions in R (where they’re easiest to manipulate), such as in a large-scale power simulation, or with packages such purrr.

# Let's combine both `future` and `purrr` with Davis Vaughan's `furrr` package instead
library(furrr)
future::plan(tweak(multiprocess, workers = 5L))

# Sexy data frame format for easy analysis!
df <- future_imap_dfr(
  possible_scenarios,
  function(x, i) {
    res <- collector(eval(x))
    data.frame(k = i,
               messages = paste(res$message, collapse=" "),
               warnings = paste(res$warning, collapse=" "),
               errors =   paste(res$error, collapse=" "),
               stringsAsFactors = FALSE) 
  })

Other goodies

catchr offers many other benefits, to the point where trying to document them all in a single vignette doesn’t make sense. I’ll point out some of the more unique ones here, but the help docs are very extensive, so give them a try!

Catching “misc” conditions

Sometimes we don’t want to have to specify every alternative condition out there, and we might just want to divide conditions into one type and “everything else”.

You can do this by making plans for the “misc” condition, a term catchr reserves for any condition(s) that you haven’t already specified. Thus:

messages_or_bust <- make_catch_fn(messages = collect, 
                                  misc = exit_with("Sorry, busted"))

will collect messages (without muffling them) but will exit the evaluation and return a conciliatory if any other types of conditions are raised. Note that using the all-purpose “condition” plan (i.e., condition = exit_with("Sorry, busted")) would exit when a message is raised, since it isn’t muffled and qualifies as having the type “condition”. Using “misc” makes sure you don’t “double-dip” conditions.

The “misc” condition is particularly useful when collecting conditions—it lets you lump all the miscellaneous conditions collected into a single “misc” sublist. Since anything other than the “big three” conditions (errors, messages, and warnings) are so rare, the following makes a nice little collector that lumps anything “exotic” into the “misc” sublist and drops any condition sublist when that condition isn’t raised:

basic_collector <- make_catch_fn(
  message, warning, error, misc,
  .opts = catchr_opts(default_plan = c(collect, muffle),
                      drop_empty_conds = T)
)

Display conditions in style

If you have the crayon package installed, you can display messages without raising them in whatever crayon stylings you want (though you won’t see the styles in this post):

make_warnings <- function() {
  warning("This warning has a call")
  warning("This warning does not", call. = FALSE)
  invisible("done")
}

res <- catch_expr(make_warnings(), warning = c(display, muffle))
## warning in `make_warnings()`: This warning has a call
## warning: This warning does not
res <- catch_expr(make_warnings(), 
           warning = c(display_with(c("pink", "underline", "bold")), muffle))
## simpleWarning in `make_warnings()`: This warning has a call
## simpleWarning: This warning does not

Play sounds when conditions are raised

Particularly useful for when you’re working in a different window and waiting for a job to finish / do something unexpected, catchr lets you play short sounds whenever conditions are raised.7 This functionality requires the beepr package, which I’ve found absolutely worth downloading.

For example, you can have it alert you when a condition is raised via sound, display the condition without raising it, and keep going:

warning_in_middle <- function() {
  Sys.sleep(2)
  message("It's time!")
  Sys.sleep(2)
  invisible("done")
}

res <- catch_expr(warning_in_middle(), condition = c(beep, display, muffle))

Or you could imagine making a wrapper that screams and stop evaluation whenever it encounters any unexpected condition:

tell_you_when_it_blows_up <- make_catch_fn(
  condition = c(display, beep_with(9), exit))

tell_you_when_it_blows_up(message("Oopsies!"))

All plans are “calling handlers”

This is definitely more of a technical point, but all the plans in catchr are technically evaluated in order. Currently (rlang 0.3.1) rlang’s condition-handling function, with_handlers, has the relatively unintuitive property of not checking its handlers in the order they are defined, but first checking all the “exiting” handlers, and then checking all the “calling” handlers. catchr actually offers a function inspired by with_handlers that strictly checks these in order and uses rlang’s schema (with_ordered_handlers), but catchr’s main condition handling does one better: it gets rid of “exiting” handlers all together.

Ok, this is beginning to get much more technical, but there are many other benefits to making all the plans (and therefore, handlers) “calling” handlers. For one, any plan has the option of restarting the code (via muffle, or invoking user-created restarts).

But each plan can also act as an “exiting” handler with the use of the exit term, or the user_exit() and exit_with() functions. In essence, catchr gives you the best of both worlds!



Source Code:

All the source code for catchr is on Github, including the vignette this is based off of!

Download the released version of catchr from CRAN with:

install.packages("catchr")

Or the development version from GitHub with:

# install.packages("devtools")
devtools::install_github("burchill/catchr")

Please feel free to drop me a line on what you think, or if you spot any bugs!

Footnotes:

  1. At least, I thought it was. Now I understand that it’s everything else that doesn’t make sense. 

  2. Accessible via help(conditions)

  3. Ed.: you can’t say that in a CRAN package 

  4. See help("catchr-DSL", "catchr") for the details. 

  5. See help(`catchr-plans`, "catchr")

  6. i.e., “suppresses”, “catches”, “hides”—whatever you want to call it 

  7. If you’re going to be doing something like this, make sure getOption("warn")==1 

Tweet

  Buy me a beer? Litecoin address: LaiZUuF4RY3PkC8VMFLu3YKvXob7ZGZ5o3