The book is designed primarily for R users who want to improve their programming skills and understanding of the language. It should also be useful for programmers coming to R from other languages, as help you to understand why R works the way it does.
So far we’ve just signalled conditions, and not looked at the objects that are created behind the scenes. The easiest way to see a condition object is to catch one from a signalled condition. That’s the job of rlang::catch_cnd()
But before we can learn about and use these handlers, we need to talk a little bit about condition objects. These are created implicitly whenever you signal a condition, but become explicit inside the handler.
Built-in conditions are lists with two elements:
message, a length-1 character vector containing the text to display to a user. To extract the message, use conditionMessage(cnd).
call, the call which triggered the condition. As described above, we don’t use the call, so it will often be NULL. To extract it, use conditionCall(cnd).
Conditions also have a class attribute, which makes them S3 objects.
The protected code is evaluated in the environment of tryCatch(), but the handler code is not, because the handlers are functions. This is important to remember if you’re trying to modify objects in the parent environment.
The handler functions are called with a single argument, the condition object. I call this argument cnd, by convention. This value is only moderately useful for the base conditions because they contain relatively little data. It’s more useful when you make your own custom conditions, as you’ll see shortly.
tryCatch() has one other argument: finally. It specifies a block of code (not a function) to run regardless of whether the initial expression succeeds or fails. This can be useful for clean up, like deleting files, or closing connections. This is functionally equivalent to using on.exit() (and indeed that’s how it’s implemented) but it can wrap smaller chunks of code than an entire function.
The handlers set up by tryCatch() are called exiting handlers, because they cause code to exit once the condition has been caught. By contrast, withCallingHandlers() sets up calling handlers: code execution continues normally once the handler returns. This tends to make withCallingHandlers() a more natural pairing with the non-error conditions. Exiting and calling handlers use “handler” in slighty different senses:
One important side-effect unique to calling handlers is the ability to muffle the signal. By default, a condition will continue to propagate to parent handlers, all the way up to the default handler (or an exiting handler, if provided):
If you want to prevent the condition “bubbling up” but still run the rest of the code in the block, you need to explicitly muffle it with rlang::cnd_muffle():