Error Handling
quack-rs reports every error through one type, ExtensionError, and the ExtResult<T>
alias. This page covers creating and propagating errors, how they reach DuckDB, and why
extension code must not panic.
ExtensionError
#![allow(unused)] fn main() { use quack_rs::error::{ExtensionError, ExtResult}; let (name, code) = ("my_fn", 1); let some_std_error = std::fmt::Error; // From a string literal let e = ExtensionError::from("something went wrong"); // From a format string let e = ExtensionError::new(format!("failed to register '{}': code {}", name, code)); // Wrapping another error let e = ExtensionError::from_error(some_std_error); }
ExtensionError implements:
std::error::ErrorDisplay,Debug,Clone,PartialEq,EqFrom<&str>,From<String>,From<Box<dyn Error>>,From<Box<dyn Error + Send + Sync>>From<std::io::Error>,From<std::ffi::NulError>,From<std::fmt::Error>
The From<std::io::Error> impl lets extensions that allocate runtime resources
during initialization (a tokio runtime, for example) use ? without .map_err():
#![allow(unused)] fn main() { use quack_rs::connection::Connection; use quack_rs::error::ExtensionError; // A stand-in with tokio's signature: `Runtime::new() -> std::io::Result<Runtime>`. mod tokio { pub mod runtime { pub struct Runtime; impl Runtime { pub fn new() -> std::io::Result<Self> { Ok(Runtime) } } } } fn register_all(con: &Connection) -> Result<(), ExtensionError> { let _rt = tokio::runtime::Runtime::new()?; // ← io::Error → ExtensionError // ... register functions ... Ok(()) } }
ExtResult<T>
A type alias for Result<T, ExtensionError>, used throughout the SDK:
#![allow(unused)] fn main() { use quack_rs::error::ExtensionError; pub type ExtResult<T> = Result<T, ExtensionError>; }
Propagating errors with ?
In your registration function:
#![allow(unused)] fn main() { use libduckdb_sys::{duckdb_connection, duckdb_data_chunk, duckdb_function_info, duckdb_vector}; use quack_rs::prelude::*; unsafe extern "C" fn my_fn(_: duckdb_function_info, _: duckdb_data_chunk, _: duckdb_vector) {} fn register(con: duckdb_connection) -> Result<(), ExtensionError> { unsafe { ScalarFunctionBuilder::try_new("my_fn")? .param(TypeId::BigInt) .returns(TypeId::BigInt) .function(my_fn) .register(con)?; // ← ? propagates registration errors SqlMacro::scalar("my_macro", &["x"], "x + 1")? .register(con)?; Ok(()) } } }
If any registration call fails, ? returns the error from register, which
init_extension then reports to DuckDB via access.set_error.
Error reporting to DuckDB
init_extension converts the ExtensionError to a C string for DuckDB's set_error
callback, following the same rule as ExtensionError::to_c_string: a C string cannot
hold a NUL byte, so each one is replaced with ? and the rest of the message is kept:
#![allow(unused)] fn main() { use quack_rs::error::ExtensionError; let err = ExtensionError::new("bad\0input"); assert_eq!(err.to_c_string().to_str(), Ok("bad?input")); }
DuckDB surfaces this string to the user as the extension load error.
No panics
The central rule of DuckDB extension development:
Never
unwrap(),expect(), orpanic!()in any code path that DuckDB may call.
A panic cannot unwind out of an extern "C" function: since Rust 1.81 the runtime aborts
the process instead, taking the user's DuckDB session with it. quack-rs's callback macros
and typed builders catch panics and turn them into SQL errors (which requires
panic = "unwind" in the release profile), but that is a safety net for bugs, not an
error-handling strategy.
Safe patterns
#![allow(unused)] fn main() { use libduckdb_sys::duckdb_aggregate_state; use quack_rs::aggregate::{AggregateState, FfiState}; use quack_rs::error::ExtensionError; #[derive(Default)] struct MyState { count: u64 } impl AggregateState for MyState {} fn some_fallible_call() -> Result<u64, ExtensionError> { Ok(1) } unsafe fn demo(state_ptr: duckdb_aggregate_state, maybe_count: Option<u64>) -> Result<(), ExtensionError> { // ✅ Use Option methods if let Some(s) = FfiState::<MyState>::with_state_mut(state_ptr) { s.count += 1; } // ✅ Use Result and ? let value = some_fallible_call()?; // ✅ Use unwrap_or / unwrap_or_else / map let count = maybe_count.unwrap_or(0); // ❌ Never in FFI callbacks let s = FfiState::<MyState>::with_state_mut(state_ptr).unwrap(); // panics if None Ok(()) } }
In init_extension
init_extension reports every error via set_error and does not panic itself. It also
runs your registration closure under catch_unwind, so a panic there becomes a load error
rather than a process abort (with panic = "unwind").