bookclub-advr

DSLC Advanced R Book Club
git clone https://git.eamoncaddigan.net/bookclub-advr.git
Log | Files | Refs | README | LICENSE

08.Rmd (11231B)


      1 ---
      2 engine: knitr
      3 title: Conditions
      4 ---
      5 
      6 ## Learning objectives:
      7 
      8 - What conditions are
      9 - How to use them
     10 
     11 ## Introduction
     12 
     13 What are conditions? Problems that happen in functions:
     14 
     15 - Error
     16 - Warning
     17 - Message
     18 
     19 As a function author, one can signal them--that is, say there's a problem.
     20 
     21 As a function consumer, one can handle them--for example, react or ignore.
     22 
     23 ## Signalling conditions
     24 
     25 ### Types of conditions
     26 
     27 Three types of conditions:
     28 
     29 - `r emoji::emoji("x")` **Errors.** Problem arose, and the function cannot continue. 
     30 - `r emoji::emoji("warning")` **Warnings.** Problem arose, but the function can continue, if only partially.
     31 - `r emoji::emoji("speech_balloon")` **Messages.** Something happened, and the user should know.
     32 
     33 ### `r emoji::emoji("x")` Errors
     34 
     35 How to throw errors
     36 
     37 ```{r throwing_errors, error = TRUE}
     38 # with base R
     39 stop("... in the name of love...")
     40 
     41 # with rlang
     42 rlang::abort("...before you break my heart...")
     43 
     44 # with base R; without call
     45 stop("... think it o-o-over...", call. = FALSE)
     46 ```
     47 Composing error messages
     48 
     49 - Mechanics.
     50   - `stop()` pastes together arguments
     51 ```{r, error = TRUE}
     52 some_val <- 1
     53 stop("Your value is: ", some_val, call. = FALSE)
     54 ```
     55   - `abort()` requires `{glue}`
     56 ```{r, error = TRUE}
     57 some_val <- 1
     58 rlang::abort(glue::glue("Your value is: {some_val}"))
     59 ```
     60 - Style. See [here](http://style.tidyverse.org/error-messages.html).
     61 
     62 ### `r emoji::emoji("warning")` Warnings
     63 
     64 May have multiple warnings per call
     65 
     66 ```{r}
     67 warn <- function() {
     68   warning("This is your first warning")
     69   warning("This is your second warning")
     70   warning("This is your LAST warning")
     71 }
     72 ```
     73 
     74 Print all warnings once call is complete.
     75 
     76 ```{r}
     77 warn()
     78 ```
     79 
     80 Like errors, `warning()` has
     81 
     82 - a call argument
     83 - an `{rlang}` analog
     84 
     85 ```{r}
     86 # base R
     87 # ... with call (implicitly .call = TRUE)
     88 warning("Warning")
     89 # ... with call suppressed
     90 warning("Warning", call. = FALSE)
     91 
     92 # rlang
     93 # note: call suppressed by default
     94 rlang::warn("Warning")
     95 ```
     96 
     97 (Hadley's) advice on usage:
     98 
     99 - Err on the side of errors. In other words, error rather than warn.
    100 - But warnings make sense in a few cases:
    101   - Function is being deprecated. Warn that it is reaching end of life.
    102   - Function is reasonably sure to recover from issue.
    103 
    104 ### `r emoji::emoji("speech_balloon")` Messages
    105 
    106 Mechanics:
    107 
    108 - Issued immediately
    109 - Do not have a call argument
    110 
    111 Style:
    112 
    113 Messages are best when they inform about:
    114 
    115 - Default arguments
    116 - Status updates of for functions used primarily for side-effects (e.g., interaction with web API, file downloaded, etc.)
    117 - Progress of long-running process (in the absence of a status bar).
    118 - Package loading message (e.g., attaching package, objects masked)
    119 
    120 ## Ignoring conditions
    121 
    122 A few ways:
    123 
    124 - `try()`
    125 - `suppressWarnings()`
    126 - `suppressMessages()`
    127 
    128 ### `try()`
    129 
    130 What it does:
    131 
    132 - Displays error
    133 - But continues execution after error
    134 
    135 ```{r}
    136 bad_log <- function(x) {
    137   try(log(x))
    138   10
    139 }
    140 
    141 bad_log("bad")
    142 ```
    143 
    144 Better ways to react to/recover from errors:
    145 
    146 1. Use `tryCatch()` to "catch" the error and perform a different action in the event of an error.
    147 1. Set a default value inside the call. See below.
    148 
    149 ```{r}
    150 default <- NULL
    151 try(default <- read.csv("possibly-bad-input.csv"), silent = TRUE)
    152 ```
    153 
    154 
    155 ### `suppressWarnings()`, `suppressMessages()`
    156 
    157 What it does:
    158 
    159 - Supresses all warnings (messages)
    160 
    161 ```{r}
    162 # suppress warnings (from our `warn()` function above)
    163 suppressWarnings(warn())
    164 
    165 # suppress messages
    166 many_messages <- function() {
    167   message("Message 1")
    168   message("Message 2")
    169   message("Message 3")
    170 }
    171 
    172 suppressMessages(many_messages())
    173 ```
    174 
    175 ## Handling conditions
    176 
    177 Every condition has a default behavior:
    178 
    179 - `r emoji::emoji("x")` Errors halt execution
    180 - `r emoji::emoji("warning")` Warnings are collected during execution and displayed in bulk after execution
    181 - `r emoji::emoji("speech_balloon")` Messages are displayed immediately
    182 
    183 Condition handlers allow one to change that behavior (within the scope of a function).
    184 
    185 Two handler functions:
    186 
    187 - `tryCatch()`
    188 - `withCallingHandlers()`
    189 
    190 ```{r, eval=FALSE}
    191 # try to run `code_to_try_to_run`
    192 # if (error) condition is signalled, fun some other code
    193 tryCatch(
    194   error = function(cnd) {
    195     # code to run when error is thrown
    196   },
    197   code_to_try_to_run
    198 )
    199 
    200 # try to `code_to_try_to_run`
    201 # if condition is signalled, run code corresponding to condition type
    202 withCallingHandlers(
    203   warning = function(cnd) {
    204     # code to run when warning is signalled
    205   },
    206   message = function(cnd) {
    207     # code to run when message is signalled
    208   },
    209   code_to_try_to_run
    210 )
    211 ```
    212 
    213 
    214 ### Condition objects
    215 
    216 ```{r}
    217 # catch a condition
    218 cnd <- rlang::catch_cnd(stop("An error"))
    219 # inspect it
    220 str(cnd)
    221 ```
    222 
    223 The standard components
    224 
    225 - `message`. The error message. To extract it, use `conditionMessage(cnd)`.
    226 - `call`. The function call that triggered the condition. To extract it, use `conditionCall(cnd)`.
    227 
    228 But custom conditions may contain other components.
    229 
    230 ### Exiting handlers
    231 
    232 If a condition is signalled, this type of handler controls what code to run before exiting the function call. 
    233 
    234 ```{r}
    235 f3 <- function(x) {
    236   tryCatch(
    237     # if error signalled, return NA
    238     error = function(cnd) NA,
    239     # try to run log
    240     log(x)
    241   )
    242 }
    243 
    244 f3("x")
    245 ```
    246 
    247 When a condition is signalled, control moves to the handler and never returns to the original code.
    248 
    249 ```{r}
    250 tryCatch(
    251   message = function(cnd) "There",
    252   {
    253     message("Here")
    254     stop("This code is never run!")
    255   }
    256 )
    257 ```
    258 
    259 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.
    260 
    261 ```{r}
    262 # try to write text to disk
    263 # if an error is signalled--for example, `path` does not exist
    264 # or if no condition is signalled
    265 # that is in both cases, the code block in `finally` is executed
    266 path <- tempfile()
    267 tryCatch(
    268   {
    269     writeLines("Hi!", path)
    270     # ...
    271   },
    272   finally = {
    273     # always run
    274     unlink(path)
    275   }
    276 )
    277 ```
    278 
    279 ### Calling handlers
    280 
    281 Definition by verbal comparison:
    282 
    283 - With exit handlers, code exits the normal flow once a condition is signalled
    284 - With calling handlers, code continues in the normal flow once control is returned by the handler.
    285 
    286 Definition by code comparison:
    287 
    288 ```{r}
    289 # with an exit handler, control moves to the handler once condition signalled and does not move back
    290 tryCatch(
    291   message = function(cnd) cat("Caught a message!\n"), 
    292   {
    293     message("Someone there?")
    294     message("Why, yes!")
    295   }
    296 )
    297 
    298 # with a calling handler, control moves first to the handler and the moves back to the main code
    299 withCallingHandlers(
    300   message = function(cnd) cat("Caught a message!\n"), 
    301   {
    302     message("Someone there?")
    303     message("Why, yes!")
    304   }
    305 )
    306 ```
    307 
    308 ### By default, conditions propagate
    309 
    310 Let's suppose that there are nested handlers. If a condition is signalled in the child, it propagates to its parent handler(s).
    311 
    312 ```{r}
    313 # Bubbles all the way up to default handler which generates the message
    314 withCallingHandlers(
    315   message = function(cnd) cat("Level 2\n"),
    316   withCallingHandlers(
    317     message = function(cnd) cat("Level 1\n"),
    318     message("Hello")
    319   )
    320 )
    321 
    322 # Bubbles up to tryCatch
    323 tryCatch(
    324   message = function(cnd) cat("Level 2\n"),
    325   withCallingHandlers(
    326     message = function(cnd) cat("Level 1\n"),
    327     message("Hello")
    328   )
    329 )
    330 ```
    331 
    332 ### But conditions can be muffled
    333 
    334 If one wants to "muffle" the siginal, one needs to use `rlang::cnd_muffle()`
    335 
    336 ```{r}
    337 # Muffles the default handler which prints the messages
    338 withCallingHandlers(
    339   message = function(cnd) {
    340     cat("Level 2\n")
    341     rlang::cnd_muffle(cnd)
    342   },
    343   withCallingHandlers(
    344     message = function(cnd) cat("Level 1\n"),
    345     message("Hello")
    346   )
    347 )
    348 
    349 # Muffles level 2 handler and the default handler
    350 withCallingHandlers(
    351   message = function(cnd) cat("Level 2\n"),
    352   withCallingHandlers(
    353     message = function(cnd) {
    354       cat("Level 1\n")
    355       rlang::cnd_muffle(cnd)
    356     },
    357     message("Hello")
    358   )
    359 )
    360 ```
    361 
    362 ### Call stacks
    363 
    364 Call stacks of exiting and calling handlers differ.
    365 
    366 Why? 
    367 
    368 > Calling handlers are called in the context of the call that signalled the condition
    369 > exiting handlers are called in the context of the call to tryCatch()
    370 
    371 To see this, consider how the call stacks differ for a toy example.
    372 
    373 ```{r}
    374 # create a function
    375 f <- function() g()
    376 g <- function() h()
    377 h <- function() message
    378 
    379 # call stack of calling handlers
    380 withCallingHandlers(f(), message = function(cnd) {
    381   lobstr::cst()
    382   rlang::cnd_muffle(cnd)
    383 })
    384 
    385 # call stack of exit handlers
    386 tryCatch(f(), message = function(cnd) lobstr::cst())
    387 tryCatch(f(), message = function(cnd) lobstr::cst())
    388 ```
    389 
    390 ## Custom conditions
    391 
    392 ### Motivation
    393 
    394 The `base::log()` function provides a minimal error message.
    395 
    396 ```{r, error = TRUE}
    397 log(letters)
    398 log(1:10, base = letters)
    399 ```
    400 
    401 One could make a more informative error message about which argument is problematic.
    402 
    403 ```{r}
    404 my_log <- function(x, base = exp(1)) {
    405   if (!is.numeric(x)) {
    406     rlang::abort(paste0(
    407       "`x` must be a numeric vector; not ", typeof(x), "."
    408     ))
    409   }
    410   if (!is.numeric(base)) {
    411     rlang::abort(paste0(
    412       "`base` must be a numeric vector; not ", typeof(base), "."
    413     ))
    414   }
    415 
    416   base::log(x, base = base)
    417 }
    418 ```
    419 
    420 Consider the difference:
    421 
    422 ```{r, error = TRUE}
    423 my_log(letters)
    424 my_log(1:10, base = letters)
    425 ```
    426 
    427 
    428 ### Signalling
    429 
    430 Create a helper function to describe errors:
    431 
    432 ```{r}
    433 abort_bad_argument <- function(arg, must, not = NULL) {
    434   msg <- glue::glue("`{arg}` must {must}")
    435   if (!is.null(not)) {
    436     not <- typeof(not)
    437     msg <- glue::glue("{msg}; not {not}.")
    438   }
    439   
    440   rlang::abort(
    441     "error_bad_argument", # <- this is the (error) class, I believe
    442     message = msg, 
    443     arg = arg, 
    444     must = must, 
    445     not = not
    446   )
    447 }
    448 ```
    449 
    450 Rewrite the log function to use this helper function:
    451 
    452 ```{r}
    453 my_log <- function(x, base = exp(1)) {
    454   if (!is.numeric(x)) {
    455     abort_bad_argument("x", must = "be numeric", not = x)
    456   }
    457   if (!is.numeric(base)) {
    458     abort_bad_argument("base", must = "be numeric", not = base)
    459   }
    460 
    461   base::log(x, base = base)
    462 }
    463 ```
    464 
    465 See the result for the end user:
    466 
    467 ```{r, error = TRUE}
    468 my_log(letters)
    469 my_log(1:10, base = letters)
    470 ```
    471 
    472 ### Handling
    473 
    474 Use class of condition object to allow for different handling of different types of errors
    475 
    476 ```{r, error = TRUE}
    477 tryCatch(
    478   error_bad_argument = function(cnd) "bad_argument",
    479   error = function(cnd) "other error",
    480   my_log("a")
    481 )
    482 ```
    483 
    484 But note that the first handler that matches any of the signal's class, potentially in a vector of signal classes, will get control. So put the most specific handlers first.
    485 
    486 ## Applications
    487 
    488 See [the sub-section in the book](https://adv-r.hadley.nz/conditions.html#condition-applications) for excellent examples.
    489 
    490 ## Resources
    491 
    492 - Conditions articles in rlang vignettes: 
    493   - [Including function calls in error messages](https://rlang.r-lib.org/reference/topic-error-call.html)
    494   - [Including contextual information with error chains](https://rlang.r-lib.org/reference/topic-error-chaining.html)
    495   - [Formatting messages with cli](https://rlang.r-lib.org/reference/topic-condition-formatting.html)
    496 - [Other resources](https://github.com/rstudio-conf-2022/pkg-dev-masterclass/blob/main/materials/5-error-resources.md) from error message segment of rstudio::conf(2022) workshop "Package Development Masterclass"