I would like to write a custom error function in R that uses a more consistent formatting of error messages. In base R, errors are formatted in a way that puts error messages on a new line if it exceeds the console width and indents it by two spaces:
abort <- function(msg) stop(msg)
> abort("This is a very very very very long error message")
# Error in abort("This is a very very very very long error message") :
# This is a very very very very long error message
If it doesn't exceed the console width, it is put directly behind the call:
> abort("Short message")
# Error in abort("Short message") : Short message
In my custom error function, I intend to keep the formatting more consistent by always starting the error messages on a new line (similar to the cli
package error format):
abort <- function(msg) stop("\n", msg)
However, when using this function with a very long error message (or a long call), the base R error formatting adds a new line and thus unwanted white space to the error output.
> abort("This is a very very very very very very very very long error message")
# Error in abort("This is a very very very very very very very very long error message") :
#
# This is a very very very very very very very very long error message
Is there any way to control this behavior, either by suppressing it or by finding out when it occurs to manually work around it?
It's easy enough to temporarily suspend error message printing with options(show.error.messages = FALSE)
, then print whatever message you like to the console. Thereafter, you can throw an error using a simple stop(msg)
to halt execution, and only your own bespoke message will appear.
The fatal issue with this approach is that after the program halts, you haven't turned the user's error printing back on, which is a big problem. You can't simply reset the user options inside abort
after calling stop()
, since the program has halted and your option-resetting code will simply not execute.
This is where base R's on.exit()
comes in. This handy function allows you to supply an arbitrary block of code to be run after your function exits, even if your function exits due to an unhandled error. We can therefore tell on.exit
to reset the user's options when we exit the abort
function, as long as we register the on.exit
instructions before we stop()
the program.
This mechanism is essentially how rlang
manages to produce error messages with bespoke formatting, via the function rlang:::signal_abort
.
We also want to examine the call stack so that we can tell the user where abort
was encountered, so a minimal implementation might be something like:
abort <- function(msg) {
sc <- sys.calls()
lsc <- length(sc)
if(lsc == 1) header <- "Error:\n"
if(lsc > 1) header <- paste0("Error in `", deparse(sc[lsc - 1][[1]]), "`:\n")
old_options <- options(show.error.messages = FALSE)
on.exit(options(old_options))
message(header, msg)
stop(msg)
}
Testing, we get:
abort("Abort messages print as desired")
#> Error:
#> Abort messages print as desired
stop("Standard error messages still print normally")
#> Error: Standard error messages still print normally
And if we encounter abort
inside a function, we can see execution is halted and we get some information about the call that led to the abort
being triggered:
test_fun <- function() {
abort("This is the error")
cat("This line should not print")
}
test_fun()
#> Error in `test_fun()`:
#> This is the error