Site icon R-bloggers

Capture errors, warnings and messages

[This article was first published on Econometrics and Free Software, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

In my last video I tried to add a feature to my {loud} package (more info here) and I succeeded. But in succeeding in realised that I would need to write a bit more code than what I expected. To make a long story short: it is possible to capture errors using purrr::safely():

library(purrr)
safe_log <- safely(log)

a <- safe_log("10")

str(a)
## List of 2
##  $ result: NULL
##  $ error :List of 2
##   ..$ message: chr "non-numeric argument to mathematical function"
##   ..$ call   : language .Primitive("log")(x, base)
##   ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"

a is now a list with elements $result and $error. If everything goes right, $result holds the result of the operation, and if everything goes wrong, $result is NULL but $error now contains the error message. This is especially useful in non-interactive contexts. There is another similar function in {purrr} called quietly(), which captures warnings and messages:

quiet_log <- quietly(log)

b <- quiet_log(-10)

str(b)
## List of 4
##  $ result  : num NaN
##  $ output  : chr ""
##  $ warnings: chr "NaNs produced"
##  $ messages: chr(0)

as you can see, providing a negative number to log() does not cause an error, but simply a warning. A result of NaN is returned (you can try with log(-10) in your console). quietly() captures the warning message and returns a list of 4 elements, $result, $output, $warnings and $messages. The problem here, is that:

safe_log(-10)
## Warning in .Primitive("log")(x, base): NaNs produced
## $result
## [1] NaN
## 
## $error
## NULL

returns something useless: $result is NaN (because that’s what log() returns for negative numbers) but $error is NULL since no error was thrown, but only a warning! We have a similar problem with quiet_log():

quiet_log("10")
Error in .Primitive("log")(x, base) : 
  non-numeric argument to mathematical function

here, the error message is thrown, but not captured, since quietly() does not capture error messages.

So, are we back to square one? Not necessarily, since you could compose both functions:

pure_log <- quietly(safely(log))

a2 <- pure_log(-10)

str(a2)
## List of 4
##  $ result  :List of 2
##   ..$ result: num NaN
##   ..$ error : NULL
##  $ output  : chr ""
##  $ warnings: chr "NaNs produced"
##  $ messages: chr(0)
b2 <- pure_log("10")

str(b2)
## List of 4
##  $ result  :List of 2
##   ..$ result: NULL
##   ..$ error :List of 2
##   .. ..$ message: chr "non-numeric argument to mathematical function"
##   .. ..$ call   : language .Primitive("log")(x, base)
##   .. ..- attr(*, "class")= chr [1:3] "simpleError" "error" "condition"
##  $ output  : chr ""
##  $ warnings: chr(0) 
##  $ messages: chr(0)

As you can see, in the case of a2, the warning was captured, and in the case of b2 the error was captured. The problem, is that the resulting object is quite complex. It’s a list where $result is itself a list in case of a warning, or $error is a list in case of an error.

I tried to write a function that would decorate a function (as do safely() and quietly()), which in turn would then return a simple list and capture, errors, warnings and messages. I came up with this code, after re-reading Advanced R, in particular this chapter:

purely <- function(.f){

  function(..., .log = "Log start..."){

    res <- rlang::try_fetch(
                    rlang::eval_tidy(.f(...)),
                    error = function(err) err,
                    warning = function(warn) warn,
                    message = function(message) message,
                    )

    final_result <- list(
      result = NULL,
      log = NULL
    )

    final_result$result <- if(any(c("error", "warning", "message") %in% class(res))){
                             NA
                           } else {
                             res
                           }

    final_result$log <- if(any(c("error", "warning", "message") %in% class(res))){
                          res$message
                        } else {
                          NA
                        }
    final_result
  }
}
f_m <- function(x){
  message("this is a message")
  str(x)
}

f_w <- function(x){
  warning("this is a warning")
  str(x)

}

f_e <- function(){
  stop("This is an error")

}

pure_fm <- purely(f_m)
pure_fw <- purely(f_w)
pure_fe <- purely(f_e)

Messages get captured:

pure_fm(10) |>
  str()
## List of 2
##  $ result: logi NA
##  $ log   : chr "this is a message\n"

as do warnings:

pure_fw(10) |>
  str()
## List of 2
##  $ result: logi NA
##  $ log   : chr "this is a warning"

as do errors:

pure_fe() |>
  str()
## List of 2
##  $ result: logi NA
##  $ log   : chr "This is an error"

The structure of the result is always $result and $log. In case everything goes well $result holds the result:

pure_log <- purely(log)

pure_log(c(1,10))
## $result
## [1] 0.000000 2.302585
## 
## $log
## [1] NA

And another example, with a more complex call:

pure_mean <- purely(mean)

pure_mean(c(1,10, NA), na.rm = TRUE)
## $result
## [1] 5.5
## 
## $log
## [1] NA

But in case something goes wrong, the error message will get captured.

suppressPackageStartupMessages(library(dplyr))
## {paint} masked print.tbl_df
pure_select <- purely(select)

Let’s try here to select a column that does not exist:

clean_mtcars <- mtcars %>%
  pure_select(hp, am, bm) #bm does not exist

str(clean_mtcars)
## List of 2
##  $ result: logi NA
##  $ log   : chr ""

Compare to what happens with select():

clean_mtcars2 <- mtcars %>%
  select(hp, am, bm) #bm does not exist
Error in `select()`:
! Can't subset columns that don't exist.
✖ Column `bm` doesn't exist.
Backtrace:
  1. mtcars %>% select(hp, am, bm)
...
...

The code (and thus the pipeline) completely fails! I’ve added this function to my {loud} package, but the biggest benefit of all this is that the main function of the package, loudly() now uses purely() under the hood to provide more useful log messages in case of failure:

suppressPackageStartupMessages(library(loud))

loud_sqrt <- loudly(sqrt)
loud_mean <- loudly(mean)
loud_exp <- loudly(exp)


result_pipe <- -1:-10 |>
  loud_mean() %>=% # This results in a negative number...
  loud_sqrt() %>=% # which sqrt() does not know how to handle
  loud_exp()

If we now inspect result_pipe, we find a complete log of what went wrong:

result_pipe
## $result
## NULL
## 
## $log
## [1] "Log start..."                                                                                                                                                            
## [2] "✔ mean(-1:-10) started at 2022-03-12 22:23:48 and ended at 2022-03-12 22:23:48"                                                                                          
## [3] "✖ CAUTION - ERROR: sqrt(.l$result) started at 2022-03-12 22:23:48 and failed at 2022-03-12 22:23:48 with following message: NaNs produced"                               
## [4] "✖ CAUTION - ERROR: exp(.l$result) started at 2022-03-12 22:23:48 and failed at 2022-03-12 22:23:48 with following message: non-numeric argument to mathematical function"

If you want to know more about {loud}, I suggest you read my previous blog post and if you need a more realistic example, take a look at this.

If you try it, please let me know!

Hope you enjoyed! If you found this blog post useful, you might want to follow me on twitter for blog post updates and buy me an espresso or paypal.me, or buy my ebook on Leanpub. You can also watch my videos on youtube. So much content for you to consoom!

  • Buy me an Espresso

    To leave a comment for the author, please follow the link and comment on their blog: Econometrics and Free Software.

    R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
    Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.