Condition system: error!, warning!, message!, condition!
miniextendr provides four macros for raising structured R conditions from Rust. They all require errorinr mode β the default for every #[miniextendr] function.
miniextendr provides four macros for raising structured R conditions from Rust.
They all require error_in_r mode β the default for every #[miniextendr]
function.
πQuick reference
| Macro | R equivalent | Default class | Unhandled behaviour |
|---|---|---|---|
error!(...) | stop() | rust_error | terminates execution |
warning!(...) | warning() | rust_warning | prints, continues |
message!(...) | message() | rust_message | prints, continues |
condition!(...) | signalCondition() | rust_condition | silent no-op |
All four support an optional class = "name" argument to prepend a custom class
for programmatic catching.
πHow it works
Each macro calls std::panic::panic_any(RCondition::...). The panic is caught by
with_r_unwind_protect_error_in_r before Rust destructors have unwound, which
recognises the RCondition payload and converts it to a tagged SEXP (4-element
list: error, kind, class, call). The generated R wrapper reads the SEXP
and dispatches to the appropriate R signal function.
The class slot carries the optional user-supplied class. When non-NULL it is
prepended to the standard layered vector.
πClass layering
class(e)
# error!("...") β c("rust_error", "simpleError", "error", "condition")
# warning!("...") β c("rust_warning", "simpleWarning", "warning", "condition")
# message!("...") β c("rust_message", "simpleMessage", "message", "condition")
# condition!("...") β c("rust_condition", "simpleCondition", "condition")
# With class = "my_err":
class(e)
# error!(class = "my_err", "...") β c("my_err", "rust_error", "simpleError", "error", "condition")πRunnable examples
πerror!()
library(miniextendr)
# Raised by: error!("something went wrong: {x}")
e <- tryCatch(demo_error("oops"), error = function(e) e)
class(e)
# [1] "rust_error" "simpleError" "error" "condition"
conditionMessage(e)
# [1] "oops"
# Specific handler:
tryCatch(demo_error("x"), rust_error = function(e) "caught by rust_error handler")
# [1] "caught by rust_error handler"πerror!() with custom class
# Raised by: error!(class = "my_error", "missing field: {name}")
tryCatch(
demo_error_custom_class("my_error", "missing field: x"),
my_error = function(e) paste("custom:", conditionMessage(e)),
rust_error = function(e) paste("rust:", conditionMessage(e))
)
# [1] "custom: missing field: x"πwarning!()
# Raised by: warning!("x is large: {x}")
# tryCatch absorbs the warning and returns the handler result:
tryCatch(demo_warning("watch out"), rust_warning = function(w) "caught!")
# [1] "caught!"
# withCallingHandlers resumes execution after the handler:
result <- withCallingHandlers(
{
demo_warning("note")
42L
},
warning = function(w) {
cat("saw:", conditionMessage(w), "\n")
invokeRestart("muffleWarning")
}
)
# saw: note
result
# [1] 42πmessage!()
# Raised by: message!("step {n} complete")
demo_message("hello")
# hello
suppressMessages(demo_message("silenced"))
# (no output)
# withCallingHandlers β muffleMessage restart stops the default printing:
withCallingHandlers(
demo_message("intercepted"),
message = function(m) {
cat("caught:", conditionMessage(m))
invokeRestart("muffleMessage")
}
)
# caught: interceptedπcondition!()
# Raised by: condition!("step 1 of 10")
# Without a handler, signalCondition returns NULL invisibly.
demo_condition("silent signal")
# NULL
# With a handler:
withCallingHandlers(
demo_condition("progress event"),
condition = function(c) cat("progress:", conditionMessage(c), "\n")
)
# progress: progress event
# NULL
# With a custom class:
withCallingHandlers(
demo_condition_custom_class("my_progress", "step 3"),
my_progress = function(c) cat("progress:", conditionMessage(c), "\n")
)
# progress: step 3
# NULLπTrait-ABI and ALTREP error class layering
Cross-package trait method panics and ALTREP r_unwind callback panics
do receive rust_* class layering, even though there is no R wrapper
to inspect a tagged SEXP. Two different mechanisms cover the two contexts:
-
Trait-ABI shims: the vtable shim returns a tagged SEXP on panic; the generated View method wrapper inspects the result and re-panics with the reconstructed [
RCondition]. The consumerβs outererror_in_rguard (every#[miniextendr]fn has one) catches the re-panic and produces the tagged SEXP for the consumerβs R wrapper. End-to-end behavior is identical to a same-package call:tryCatch(rust_error = h, ...)matches; user classes fromerror!(class = "...", ...)match beforerust_error. -
ALTREP
r_unwindcallbacks: the guard raises the R condition by evaluatingstop(structure(list(message, call), class = c(...)))directly (no R wrapper required).tryCatch(rust_error = h, ...)matches; user classes match beforerust_error.
πRemaining limitations
Two narrow cases still degrade:
-
warning!()/message!()/condition!()from an ALTREPr_unwindcallback. There is no mechanism to suspend execution to deliver a non-fatal signal from inside Rβs vector-dispatch machinery. These produce an R error with the message: βwarning!/message!/condition! from ALTREP callback context cannot be raised as non-fatal signals; use error!() insteadβ. -
A trait View method (
view.method()) called from Rust code that is not wrapped inwith_r_unwind_protect_error_in_r(e.g., a manual call from a test harness or init callback). The re-panic from the View has no outer guard to catch it, so the worker threadβscatch_unwindboundary converts it to an R error withoutrust_*class layering. In practice, every#[miniextendr]fn already provides the outer guard, so this only affects unusual call sites.
Functions that explicitly opt out of error_in_r via
#[miniextendr(no_error_in_r)] or unwrap_in_r continue to use direct
Rf_errorcall β those modes exist precisely to bypass the condition
pipeline.
πAsRError β wrapping std::error::Error
For functions that return Result<T, E> where E: std::error::Error,
AsRError<E> wraps the error and formats its full cause chain into the
message:
use miniextendr_api::condition::AsRError;
use miniextendr_api::miniextendr;
#[miniextendr]
fn parse_number(s: &str) -> Result<i32, AsRError<std::num::ParseIntError>> {
s.parse::<i32>().map_err(AsRError)
}tryCatch(parse_number("abc"), error = function(e) e$message)
# [1] "invalid digit found in string"
For errors with a source chain, all causes appear in the message separated by
\n caused by: ....