28 Oct. 2019

Why raise conditions?

  • Fail fast: if something’s wrong, you want to know immediately.
  • Have confidence the code works as intended.

Conditions in the wild

library(readr)
log(-1)
## Warning in log(-1): NaNs produced
## [1] NaN
this_object_DNE
## Error in eval(expr, envir, enclos): object 'this_object_DNE' not found
readr::read_csv("inst/extdata/baxter.metadata.csv")
## Parsed with column specification:
## cols(
##   sample = col_double(),
##   fit_result = col_double(),
##   Site = col_character(),
##   Dx_Bin = col_character(),
##   dx = col_character(),
##   Hx_Prev = col_double(),
##   Hx_of_Polyps = col_double(),
##   Age = col_double(),
##   Gender = col_character(),
##   Smoke = col_double(),
##   Diabetic = col_double(),
##   Hx_Fam_CRC = col_double(),
##   Height = col_double(),
##   Weight = col_double(),
##   NSAID = col_double(),
##   Diabetes_Med = col_double(),
##   stage = col_double()
## )

How to raise conditions

library(rlang)
message("This is a benign message.")
## This is a benign message.
warning("Something could be wrong, but we can continue.")
## Warning: Something could be wrong, but we can continue.
abort("The program can't go on, burn it all down!")
## The program can't go on, burn it all down!

4th type of condition: Interrupt: when you press “Stop” or ctrl-c.

Example

my_log <- function(x, base = exp(1)) {
  if (!is.numeric(x)) {
    abort(paste0(
      "`x` must be numeric; not ", typeof(x), "."
    ))
  }
  if (!is.numeric(base)) {
    abort(paste0(
      "`base` must be numeric; not ", typeof(base), "."
    ))
  }

  base::log(x, base = base)
}
my_log('abc')
## `x` must be numeric; not character.

How to handle conditions: tryCatch

tryCatch(
  error = function(cnd) {
     # do this if you catch an error
    code_to_run_when_error_is_thrown
  },
  {
  # try this
  code_to_run_while_handlers_are_active
  }
)

Why? When you want to modify the default behavior.

tryCatch doesn’t return to the try after Catching an error

tryCatch(
  error = function(cnd) { # catch an error
      message("Oh no, there's an error!")     
    },
  { # try this
    bad_thing_happened = TRUE
    if( bad_thing_happened) {
        abort("We've gotta kill the program")
    }
    message("This code is never run!")
  }
)
## Oh no, there's an error!
# the code outside the tryCatch block continues on afterward

You can access the condition message in the Catch

tryCatch(
  error = function(cnd) { # catch an error
      message("Oh no, there's an error!")      
      message(conditionMessage(cnd))
    },
  { # try this
    bad_thing_happened = TRUE
    if( bad_thing_happened) {
        abort("We've gotta kill the program")
    }
    message("This code is never run!")
  }
)
## Oh no, there's an error!
## We've gotta kill the program
# the code outside the tryCatch block continues on afterward

How to handle conditions: withCallingHandlers

withCallingHandlers(
  warning = function(cnd) {
    code_to_run_when_warning_is_signalled
  },
  message = function(cnd) {
    code_to_run_when_message_is_signalled
  },
  code_to_run_while_handlers_are_active
)

withCallingHandlers returns to the try after a Catch

withCallingHandlers(
  warning = function(cnd) {
      message("Caught a warning!")
      # do something special
    },
  {
    warning("Something might be wrong.\n")
    message("But let's keep going anyway.")
  }
)
## Caught a warning!
## Warning in withCallingHandlers(warning = function(cnd) {: Something might be wrong.
## But let's keep going anyway.

How to write custom conditions

library(glue)  # Write a function that wraps `abort`
abort_bad_argument <- function(arg, must, not = NULL) {
  # create a custom message
  msg <- glue::glue("`{arg}` must {must}")
  # append to the message if `not` was specified
  if (!is.null(not)) {
    not <- typeof(not)
    msg <- glue::glue("{msg}; not {not}.")
  }
  # pass custom metadata to abort
  abort("error_bad_argument", 
    message = msg, 
    arg = arg, 
    must = must, 
    not = not
  )
}

Using our custom condition

my_log <- function(x, base = exp(1)) {
  if (!is.numeric(x)) {
    abort_bad_argument("x", must = "be numeric", not = x)
  }
  if (!is.numeric(base)) {
    abort_bad_argument("base", must = "be numeric", not = base)
  }

  base::log(x, base = base)
}
my_log(3, 'abc')
## `base` must be numeric; not character.

Best practices

Worst Practices

  • Wrapping your entire script in a tryCatch.
  • Catching errors or warnings and doing nothing with them.
  • Handling a condition when you should write a conditional (if-else).

Activity

Let’s write conditions for the code from the Riffomonas minimalR tutorial and work on a few exercises from AdvancedR.

  • R/baxter.R
    • get_metadata()
    • get_bmi()
    • get_bmi_category()
  • R/exercises.R
    • careful_remove()
    • check_dependencies()
    • bottles_of_beer()

Activity

  1. Clone this repo

    git clone https://github.com/SchlossLab/exception-handling

    or if you previously cloned it, pull new commits:

    cd path/to/repo/ ; git pull
  2. Checkout a new branch

    git checkout -b descriptive-branch-name
  3. After modifying your part, commit your changes

    git add . ; git commit -m "descriptive commit message"

Activity Wrap-up

  1. Push your changes

    git push -u origin descriptive-branch-name
  2. Open a pull request on GitHub, mention your issue number(s), and merge it if there aren’t conflicts.

Additional Reading