Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 ? and Box<dyn Error>
  • Display — joins all records with "; "
  • Send — safe to transfer across thread boundaries

Key methods:

MethodDescription
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: ErrState is intended for advanced use cases. Most applications do not need it; ErrorStack is Send and 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.