Error Handling
The OpenSSL error model
OpenSSL maintains a per-thread error queue. When a C function fails it pushes one
or more error records onto the queue before returning an error indicator (0 or NULL).
The queue must be drained before the next operation, or stale errors accumulate.
native-ossl drains the queue immediately after every failing C call, captures all
records, and returns them as a single ErrorStack value inside Err(...).
Types
Error — one record
A single entry from the OpenSSL error queue.
#![allow(unused)]
fn main() {
pub struct Error {
code: u64, // packed OpenSSL error code
reason: Option<String>, // human-readable reason (e.g. "unsupported algorithm")
lib: Option<String>, // library name (e.g. "EVP routines")
file: Option<String>, // C source file where the error was raised
func: Option<String>, // C function name
data: Option<String>, // caller-supplied context string (if any)
}
}
Accessors: code(), lib(), reason(), file(), func(), data() — all return
Option<&str> (or u64 for code).
Error implements Display in the form:
reason (lib) in func: data
ErrorStack — the queue snapshot
#![allow(unused)]
fn main() {
pub struct ErrorStack(Vec<Error>);
}
ErrorStack implements:
std::error::Error— works with?andBox<dyn Error>Display— joins all records with"; "Send— safe to transfer across thread boundaries
Key methods:
| Method | Description |
|---|---|
errors() | Iterator over the individual Error records |
is_empty() | true if the queue was empty when captured |
Inspecting errors
#![allow(unused)]
fn main() {
let err: ErrorStack = some_failing_call().unwrap_err();
// Print the full stack.
eprintln!("{err}");
// Iterate and inspect individual records.
for e in err.errors() {
eprintln!(
" lib={:?} reason={:?} func={:?}",
e.lib(), e.reason(), e.func()
);
}
}
Pattern at call sites
Every public function that may fail follows this pattern internally:
#![allow(unused)]
fn main() {
pub fn update(&mut self, data: &[u8]) -> Result<(), ErrorStack> {
crate::ossl_call!(EVP_DigestUpdate(
self.ptr,
data.as_ptr() as *const _,
data.len(),
));
Ok(())
}
}
The ossl_call! macro calls the C function, checks the return value, and calls
ErrorStack::drain() on failure. The ossl_ptr! variant does the same but for
functions that signal failure by returning NULL.
Cross-thread error capture
The OpenSSL error queue is thread-local. If a blocking crypto operation runs on a
worker thread and the future is polled on a different thread (common in tokio), the
error queue can be lost. Use ErrState to move errors across a thread boundary:
#![allow(unused)]
fn main() {
use native_ossl::error::ErrState;
let handle = std::thread::spawn(|| {
let result = some_crypto_op();
// Capture the queue before this thread exits.
let state = ErrState::capture();
(result, state)
});
let (result, state) = handle.join().unwrap();
// Restore the queue on the calling thread before inspecting the error.
state.restore_and_drain();
if let Err(e) = result {
eprintln!("crypto error: {e}");
}
}
Note:
ErrStateis intended for advanced use cases. Most applications do not need it;ErrorStackisSendand can be moved freely between threads as a value.
Never ignore the error queue
Even when a failure is expected or intentional, never leave the queue populated.
Call ERR_clear_error() (via sys::ERR_clear_error()) before speculative operations,
or let the next ossl_call! drain it automatically.