html.json (19136B)
1 { 2 "hash": "4732d46753d0ca29c20f998ad3e58e75", 3 "result": { 4 "engine": "knitr", 5 "markdown": "---\nengine: knitr\ntitle: Conditions\n---\n\n## Learning objectives:\n\n- What conditions are\n- How to use them\n\n## Introduction\n\nWhat are conditions? Problems that happen in functions:\n\n- Error\n- Warning\n- Message\n\nAs a function author, one can signal them--that is, say there's a problem.\n\nAs a function consumer, one can handle them--for example, react or ignore.\n\n## Signalling conditions\n\n### Types of conditions\n\nThree types of conditions:\n\n- ❌ **Errors.** Problem arose, and the function cannot continue. \n- ⚠️ **Warnings.** Problem arose, but the function can continue, if only partially.\n- 💬 **Messages.** Something happened, and the user should know.\n\n### ❌ Errors\n\nHow to throw errors\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# with base R\nstop(\"... in the name of love...\")\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: ... in the name of love...\n```\n\n\n:::\n\n```{.r .cell-code}\n# with rlang\nrlang::abort(\"...before you break my heart...\")\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error:\n#> ! ...before you break my heart...\n```\n\n\n:::\n\n```{.r .cell-code}\n# with base R; without call\nstop(\"... think it o-o-over...\", call. = FALSE)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: ... think it o-o-over...\n```\n\n\n:::\n:::\n\nComposing error messages\n\n- Mechanics.\n - `stop()` pastes together arguments\n\n::: {.cell}\n\n```{.r .cell-code}\nsome_val <- 1\nstop(\"Your value is: \", some_val, call. = FALSE)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error: Your value is: 1\n```\n\n\n:::\n:::\n\n - `abort()` requires `{glue}`\n\n::: {.cell}\n\n```{.r .cell-code}\nsome_val <- 1\nrlang::abort(glue::glue(\"Your value is: {some_val}\"))\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error:\n#> ! Your value is: 1\n```\n\n\n:::\n:::\n\n- Style. See [here](http://style.tidyverse.org/error-messages.html).\n\n### ⚠️ Warnings\n\nMay have multiple warnings per call\n\n\n::: {.cell}\n\n```{.r .cell-code}\nwarn <- function() {\n warning(\"This is your first warning\")\n warning(\"This is your second warning\")\n warning(\"This is your LAST warning\")\n}\n```\n:::\n\n\nPrint all warnings once call is complete.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nwarn()\n```\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Warning in warn(): This is your first warning\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Warning in warn(): This is your second warning\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Warning in warn(): This is your LAST warning\n```\n\n\n:::\n:::\n\n\nLike errors, `warning()` has\n\n- a call argument\n- an `{rlang}` analog\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# base R\n# ... with call (implicitly .call = TRUE)\nwarning(\"Warning\")\n```\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Warning: Warning\n```\n\n\n:::\n\n```{.r .cell-code}\n# ... with call suppressed\nwarning(\"Warning\", call. = FALSE)\n```\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Warning: Warning\n```\n\n\n:::\n\n```{.r .cell-code}\n# rlang\n# note: call suppressed by default\nrlang::warn(\"Warning\")\n```\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Warning: Warning\n```\n\n\n:::\n:::\n\n\n(Hadley's) advice on usage:\n\n- Err on the side of errors. In other words, error rather than warn.\n- But warnings make sense in a few cases:\n - Function is being deprecated. Warn that it is reaching end of life.\n - Function is reasonably sure to recover from issue.\n\n### 💬 Messages\n\nMechanics:\n\n- Issued immediately\n- Do not have a call argument\n\nStyle:\n\nMessages are best when they inform about:\n\n- Default arguments\n- Status updates of for functions used primarily for side-effects (e.g., interaction with web API, file downloaded, etc.)\n- Progress of long-running process (in the absence of a status bar).\n- Package loading message (e.g., attaching package, objects masked)\n\n## Ignoring conditions\n\nA few ways:\n\n- `try()`\n- `suppressWarnings()`\n- `suppressMessages()`\n\n### `try()`\n\nWhat it does:\n\n- Displays error\n- But continues execution after error\n\n\n::: {.cell}\n\n```{.r .cell-code}\nbad_log <- function(x) {\n try(log(x))\n 10\n}\n\nbad_log(\"bad\")\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Error in log(x) : non-numeric argument to mathematical function\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] 10\n```\n\n\n:::\n:::\n\n\nBetter ways to react to/recover from errors:\n\n1. Use `tryCatch()` to \"catch\" the error and perform a different action in the event of an error.\n1. Set a default value inside the call. See below.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ndefault <- NULL\ntry(default <- read.csv(\"possibly-bad-input.csv\"), silent = TRUE)\n```\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Warning in file(file, \"rt\"): cannot open file 'possibly-bad-input.csv': No such\n#> file or directory\n```\n\n\n:::\n:::\n\n\n\n### `suppressWarnings()`, `suppressMessages()`\n\nWhat it does:\n\n- Supresses all warnings (messages)\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# suppress warnings (from our `warn()` function above)\nsuppressWarnings(warn())\n\n# suppress messages\nmany_messages <- function() {\n message(\"Message 1\")\n message(\"Message 2\")\n message(\"Message 3\")\n}\n\nsuppressMessages(many_messages())\n```\n:::\n\n\n## Handling conditions\n\nEvery condition has a default behavior:\n\n- ❌ Errors halt execution\n- ⚠️ Warnings are collected during execution and displayed in bulk after execution\n- 💬 Messages are displayed immediately\n\nCondition handlers allow one to change that behavior (within the scope of a function).\n\nTwo handler functions:\n\n- `tryCatch()`\n- `withCallingHandlers()`\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# try to run `code_to_try_to_run`\n# if (error) condition is signalled, fun some other code\ntryCatch(\n error = function(cnd) {\n # code to run when error is thrown\n },\n code_to_try_to_run\n)\n\n# try to `code_to_try_to_run`\n# if condition is signalled, run code corresponding to condition type\nwithCallingHandlers(\n warning = function(cnd) {\n # code to run when warning is signalled\n },\n message = function(cnd) {\n # code to run when message is signalled\n },\n code_to_try_to_run\n)\n```\n:::\n\n\n\n### Condition objects\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# catch a condition\ncnd <- rlang::catch_cnd(stop(\"An error\"))\n# inspect it\nstr(cnd)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> List of 2\n#> $ message: chr \"An error\"\n#> $ call : language force(expr)\n#> - attr(*, \"class\")= chr [1:3] \"simpleError\" \"error\" \"condition\"\n```\n\n\n:::\n:::\n\n\nThe standard components\n\n- `message`. The error message. To extract it, use `conditionMessage(cnd)`.\n- `call`. The function call that triggered the condition. To extract it, use `conditionCall(cnd)`.\n\nBut custom conditions may contain other components.\n\n### Exiting handlers\n\nIf a condition is signalled, this type of handler controls what code to run before exiting the function call. \n\n\n::: {.cell}\n\n```{.r .cell-code}\nf3 <- function(x) {\n tryCatch(\n # if error signalled, return NA\n error = function(cnd) NA,\n # try to run log\n log(x)\n )\n}\n\nf3(\"x\")\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] NA\n```\n\n\n:::\n:::\n\n\nWhen a condition is signalled, control moves to the handler and never returns to the original code.\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntryCatch(\n message = function(cnd) \"There\",\n {\n message(\"Here\")\n stop(\"This code is never run!\")\n }\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] \"There\"\n```\n\n\n:::\n:::\n\n\nThe `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.\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# try to write text to disk\n# if an error is signalled--for example, `path` does not exist\n# or if no condition is signalled\n# that is in both cases, the code block in `finally` is executed\npath <- tempfile()\ntryCatch(\n {\n writeLines(\"Hi!\", path)\n # ...\n },\n finally = {\n # always run\n unlink(path)\n }\n)\n```\n:::\n\n\n### Calling handlers\n\nDefinition by verbal comparison:\n\n- With exit handlers, code exits the normal flow once a condition is signalled\n- With calling handlers, code continues in the normal flow once control is returned by the handler.\n\nDefinition by code comparison:\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# with an exit handler, control moves to the handler once condition signalled and does not move back\ntryCatch(\n message = function(cnd) cat(\"Caught a message!\\n\"), \n {\n message(\"Someone there?\")\n message(\"Why, yes!\")\n }\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Caught a message!\n```\n\n\n:::\n\n```{.r .cell-code}\n# with a calling handler, control moves first to the handler and the moves back to the main code\nwithCallingHandlers(\n message = function(cnd) cat(\"Caught a message!\\n\"), \n {\n message(\"Someone there?\")\n message(\"Why, yes!\")\n }\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Caught a message!\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Someone there?\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Caught a message!\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Why, yes!\n```\n\n\n:::\n:::\n\n\n### By default, conditions propagate\n\nLet's suppose that there are nested handlers. If a condition is signalled in the child, it propagates to its parent handler(s).\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# Bubbles all the way up to default handler which generates the message\nwithCallingHandlers(\n message = function(cnd) cat(\"Level 2\\n\"),\n withCallingHandlers(\n message = function(cnd) cat(\"Level 1\\n\"),\n message(\"Hello\")\n )\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Level 1\n#> Level 2\n```\n\n\n:::\n\n::: {.cell-output .cell-output-stderr}\n\n```\n#> Hello\n```\n\n\n:::\n\n```{.r .cell-code}\n# Bubbles up to tryCatch\ntryCatch(\n message = function(cnd) cat(\"Level 2\\n\"),\n withCallingHandlers(\n message = function(cnd) cat(\"Level 1\\n\"),\n message(\"Hello\")\n )\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Level 1\n#> Level 2\n```\n\n\n:::\n:::\n\n\n### But conditions can be muffled\n\nIf one wants to \"muffle\" the siginal, one needs to use `rlang::cnd_muffle()`\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# Muffles the default handler which prints the messages\nwithCallingHandlers(\n message = function(cnd) {\n cat(\"Level 2\\n\")\n rlang::cnd_muffle(cnd)\n },\n withCallingHandlers(\n message = function(cnd) cat(\"Level 1\\n\"),\n message(\"Hello\")\n )\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Level 1\n#> Level 2\n```\n\n\n:::\n\n```{.r .cell-code}\n# Muffles level 2 handler and the default handler\nwithCallingHandlers(\n message = function(cnd) cat(\"Level 2\\n\"),\n withCallingHandlers(\n message = function(cnd) {\n cat(\"Level 1\\n\")\n rlang::cnd_muffle(cnd)\n },\n message(\"Hello\")\n )\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> Level 1\n```\n\n\n:::\n:::\n\n\n### Call stacks\n\nCall stacks of exiting and calling handlers differ.\n\nWhy? \n\n> Calling handlers are called in the context of the call that signalled the condition\n> exiting handlers are called in the context of the call to tryCatch()\n\nTo see this, consider how the call stacks differ for a toy example.\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# create a function\nf <- function() g()\ng <- function() h()\nh <- function() message\n\n# call stack of calling handlers\nwithCallingHandlers(f(), message = function(cnd) {\n lobstr::cst()\n rlang::cnd_muffle(cnd)\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> function (..., domain = NULL, appendLF = TRUE) \n#> {\n#> cond <- if (...length() == 1L && inherits(..1, \"condition\")) {\n#> if (nargs() > 1L) \n#> warning(\"additional arguments ignored in message()\")\n#> ..1\n#> }\n#> else {\n#> msg <- .makeMessage(..., domain = domain, appendLF = appendLF)\n#> call <- sys.call()\n#> simpleMessage(msg, call)\n#> }\n#> defaultHandler <- function(c) {\n#> cat(conditionMessage(c), file = stderr(), sep = \"\")\n#> }\n#> withRestarts({\n#> signalCondition(cond)\n#> defaultHandler(cond)\n#> }, muffleMessage = function() NULL)\n#> invisible()\n#> }\n#> <bytecode: 0x000002ba6a952b98>\n#> <environment: namespace:base>\n```\n\n\n:::\n\n```{.r .cell-code}\n# call stack of exit handlers\ntryCatch(f(), message = function(cnd) lobstr::cst())\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> function (..., domain = NULL, appendLF = TRUE) \n#> {\n#> cond <- if (...length() == 1L && inherits(..1, \"condition\")) {\n#> if (nargs() > 1L) \n#> warning(\"additional arguments ignored in message()\")\n#> ..1\n#> }\n#> else {\n#> msg <- .makeMessage(..., domain = domain, appendLF = appendLF)\n#> call <- sys.call()\n#> simpleMessage(msg, call)\n#> }\n#> defaultHandler <- function(c) {\n#> cat(conditionMessage(c), file = stderr(), sep = \"\")\n#> }\n#> withRestarts({\n#> signalCondition(cond)\n#> defaultHandler(cond)\n#> }, muffleMessage = function() NULL)\n#> invisible()\n#> }\n#> <bytecode: 0x000002ba6a952b98>\n#> <environment: namespace:base>\n```\n\n\n:::\n\n```{.r .cell-code}\ntryCatch(f(), message = function(cnd) lobstr::cst())\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> function (..., domain = NULL, appendLF = TRUE) \n#> {\n#> cond <- if (...length() == 1L && inherits(..1, \"condition\")) {\n#> if (nargs() > 1L) \n#> warning(\"additional arguments ignored in message()\")\n#> ..1\n#> }\n#> else {\n#> msg <- .makeMessage(..., domain = domain, appendLF = appendLF)\n#> call <- sys.call()\n#> simpleMessage(msg, call)\n#> }\n#> defaultHandler <- function(c) {\n#> cat(conditionMessage(c), file = stderr(), sep = \"\")\n#> }\n#> withRestarts({\n#> signalCondition(cond)\n#> defaultHandler(cond)\n#> }, muffleMessage = function() NULL)\n#> invisible()\n#> }\n#> <bytecode: 0x000002ba6a952b98>\n#> <environment: namespace:base>\n```\n\n\n:::\n:::\n\n\n## Custom conditions\n\n### Motivation\n\nThe `base::log()` function provides a minimal error message.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlog(letters)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error in log(letters): non-numeric argument to mathematical function\n```\n\n\n:::\n\n```{.r .cell-code}\nlog(1:10, base = letters)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error in log(1:10, base = letters): non-numeric argument to mathematical function\n```\n\n\n:::\n:::\n\n\nOne could make a more informative error message about which argument is problematic.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmy_log <- function(x, base = exp(1)) {\n if (!is.numeric(x)) {\n rlang::abort(paste0(\n \"`x` must be a numeric vector; not \", typeof(x), \".\"\n ))\n }\n if (!is.numeric(base)) {\n rlang::abort(paste0(\n \"`base` must be a numeric vector; not \", typeof(base), \".\"\n ))\n }\n\n base::log(x, base = base)\n}\n```\n:::\n\n\nConsider the difference:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmy_log(letters)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error in `my_log()`:\n#> ! `x` must be a numeric vector; not character.\n```\n\n\n:::\n\n```{.r .cell-code}\nmy_log(1:10, base = letters)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error in `my_log()`:\n#> ! `base` must be a numeric vector; not character.\n```\n\n\n:::\n:::\n\n\n\n### Signalling\n\nCreate a helper function to describe errors:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nabort_bad_argument <- function(arg, must, not = NULL) {\n msg <- glue::glue(\"`{arg}` must {must}\")\n if (!is.null(not)) {\n not <- typeof(not)\n msg <- glue::glue(\"{msg}; not {not}.\")\n }\n \n rlang::abort(\n \"error_bad_argument\", # <- this is the (error) class, I believe\n message = msg, \n arg = arg, \n must = must, \n not = not\n )\n}\n```\n:::\n\n\nRewrite the log function to use this helper function:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmy_log <- function(x, base = exp(1)) {\n if (!is.numeric(x)) {\n abort_bad_argument(\"x\", must = \"be numeric\", not = x)\n }\n if (!is.numeric(base)) {\n abort_bad_argument(\"base\", must = \"be numeric\", not = base)\n }\n\n base::log(x, base = base)\n}\n```\n:::\n\n\nSee the result for the end user:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmy_log(letters)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error in `abort_bad_argument()`:\n#> ! `x` must be numeric; not character.\n```\n\n\n:::\n\n```{.r .cell-code}\nmy_log(1:10, base = letters)\n```\n\n::: {.cell-output .cell-output-error}\n\n```\n#> Error in `abort_bad_argument()`:\n#> ! `base` must be numeric; not character.\n```\n\n\n:::\n:::\n\n\n### Handling\n\nUse class of condition object to allow for different handling of different types of errors\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntryCatch(\n error_bad_argument = function(cnd) \"bad_argument\",\n error = function(cnd) \"other error\",\n my_log(\"a\")\n)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n#> [1] \"bad_argument\"\n```\n\n\n:::\n:::\n\n\nBut 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.\n\n## Applications\n\nSee [the sub-section in the book](https://adv-r.hadley.nz/conditions.html#condition-applications) for excellent examples.\n\n## Resources\n\n- Conditions articles in rlang vignettes: \n - [Including function calls in error messages](https://rlang.r-lib.org/reference/topic-error-call.html)\n - [Including contextual information with error chains](https://rlang.r-lib.org/reference/topic-error-chaining.html)\n - [Formatting messages with cli](https://rlang.r-lib.org/reference/topic-condition-formatting.html)\n- [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\"\n", 6 "supporting": [ 7 "08_files" 8 ], 9 "filters": [ 10 "rmarkdown/pagebreak.lua" 11 ], 12 "includes": {}, 13 "engineDependencies": {}, 14 "preserve": {}, 15 "postProcess": true 16 } 17 }