Capture errors, warnings and messages
RIn 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_bare(.f(...)),
error = function(err) err,
#rlang_error = function(rlerr) rlerr,
warning = function(warn) warn,
message = function(message) message,
)
final_result <- list(
result = NULL,
log = NULL
)
final_result$result <- if(any(c("error", "rlang_error", "warning", "message") %in% class(res))){
NA
} else {
res
}
final_result$log <- if(any(c("error", "rlang_error", "warning", "message") %in% class(res))){
#res$message
purrr::pluck(res, "message", .default = "undefined error")
} 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)
...
...
Update 2022-03-13
After writing this post I realised that the error message of select does not get captured.
This is the only example I’ve found where the error message does not get caught. This
seems to be related to the fact that tidyverse function have their own class of error
messages that inherit from error
. For some reason, there are no issues with other
functions, for example:
purely(group_by)(mtcars, bm)
## $result
## [1] NA
##
## $log
##
## "Must group by variables found in `.data`."
I will need to solve this…
Post continued…
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-13 14:17:30 and ended at 2022-03-13 14:17:30"
## [3] "✖ CAUTION - ERROR: sqrt(.l$result) started at 2022-03-13 14:17:30 and failed at 2022-03-13 14:17:30 with following message: NaNs produced"
## [4] "✖ CAUTION - ERROR: exp(.l$result) started at 2022-03-13 14:17:30 and failed at 2022-03-13 14:17:30 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!