better-duck-core returns one Result type, and engine
failures carry DuckDB’s own typed classification. This page is the overview: the shape of Error,
how the common failures surface, and the two behaviors — appender poisoning and the panic = "abort"
caveat — that most often surprise people. For the exhaustive variant list and the extension API, see
Errors & Extensions.
One Result type
better_duck_core::error::Error is #[non_exhaustive] — match it with a _ arm. It implements
std::error::Error (with source()), Display, and a partial PartialEq, so it drops into
anyhow / thiserror and Box<dyn Error>-based error handling cleanly.
How failures are grouped
The variants fall into a few families. The main runtime failure isEngine.
EngineError — DuckDB’s classification
A failed query surfaces asError::Engine(EngineError { kind, message }). Both fields are copied
out of DuckDB memory when the error is built, so the error stays valid after the result handle —
and the connection — are gone.
EngineErrorKind mirrors DuckDB’s duckdb_error_type and is #[non_exhaustive]:
- Known kinds map 1:1 (
Syntax,Catalog,Constraint, …; the full set isEngineErrorKind::KNOWN). Unknown(raw)preserves a classification a future DuckDB adds — nothing is lost across engine upgrades.Unavailableis an explicit absence: the originating API exposed only a status code and a message, with no typed classification. The driver never invents a kind by parsing message text.
Appender poisoning
TheAppender has a failure mode worth calling out: DuckDB invalidates it the
moment a flush, row-end, or close fails, and the buffered data is gone.
A Rust-side value-conversion failure mid-row also poisons the appender, but returns the original
conversion error unchanged rather than a poison message.
Async and pool errors
- The async facade runs work on
spawn_blocking; a panicked or cancelled task surfaces asError::BackgroundTaskFailed(String). - The r2d2 pool’s own builder/checkout errors are
r2d2’s type; map them into the core error with
Error::Pool(e.to_string()). A checkout timeout likewise arrives asError::Pool(..).
Panics and the abort profile
This is the single most important runtime gotcha for UDFs and other callbacks. The fix is to make fallible UDFs return errors instead of panicking — the error path never depends on unwinding:Result::Err return fails the query with E’s message; duck_bail!("…") is shorthand for
return Err(format!(…).into()).
Related
Errors & Extensions
The exhaustive
Error variant table and the extension API.Appender
Full poisoning and flush semantics.
User-defined functions
Returning errors from UDFs and the panic rules.
Async & pooling
BackgroundTaskFailed and pool errors.