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"