Skip to content

Error observers

Error observers are a mechanism to intercept errors.
They are primarily designed for error reporting—e.g. you can use them to log errors, increment a metric counter, etc.

src/core/error_observer.rs
pub async fn error_logger(e: &pavex::Error) {
    tracing::error!(
        error.msg = %e,
        error.details = ?e,
        "An error occurred"
    );
}

Registration

You register an error observer using the Blueprint::error_observer method.

src/core/blueprint.rs
use pavex::blueprint::router::POST;
use pavex::blueprint::Blueprint;
use pavex::f;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.error_observer(f!(super::error_logger));
    // [...]
}

You must provide an [unambiguous path] to the error observer, wrapped in the f! macro.
You can register as many error observers as you want: they'll all be called when an error occurs, in the order they were registered. They are invoked after the relevant error handler has been called, but before the response is sent back to the client.

Registration syntax

You can use free functions, static methods, non-static methods, and trait methods as error handlers. Check out the dependency injection cookbook for more details on the syntax for each case.

pavex::Error

Error observers must take a reference to pavex::Error as one of their input parameters.

src/core/error_observer.rs
pub async fn error_logger(e: &pavex::Error) {
    tracing::error!(
        error.msg = %e,
        error.details = ?e,
        "An error occurred"
    );
}

pavex::Error is an opaque error type—it's a wrapper around the actual error type returned by the component that failed.
It implements the Error trait from the standard library, so you can use its methods to extract information about the error (e.g. source, Display and Debug representations, etc.).
If you need to access the underlying error type, you can use the inner_ref method and then try to downcast it.

Return type

The primary purpose of error observers is to perform side effects, not to produce a value.
Therefore, they are expected to return the unit type, ()—i.e. they don't return anything.

Dependency injection

Error observers can take advantage of dependency injection.

src/di/error_observer.rs
pub async fn error_logger(e: &pavex::Error, root_span: &RootSpan /* (1)! */) {
    root_span.record("error.msg", tracing::field::display(e));
    root_span.record("error.details", tracing::field::debug(e));
}
  1. &RootSpan is injected into the error observer by the framework.

You must specify the dependencies of your error observer as input parameters in its function signature.
Those inputs are going to be built and injected by the framework, according to the constructors you have registered.

Check out the dependency injection guide for more details on how the process works.

Strictly infallible

Just like error handlers, error observers can't be fallible—they can't return a Result.
It goes further than that, though: they can't depend on fallible components, neither directly nor indirectly.
This constraint is necessary to avoid infinite loops.

Consider this scenario: you register an error observer that depends on a type A, and A's constructor can fail.
Something fails in the request processing pipeline:

  • You want to invoke the error observer: you need to build A.
    • You invoke A's constructor, but it fails!
      • You must now invoke the error observer on the error returned by A's constructor.
        • But to invoke the error observer, you need A!
          • You try to construct A again to report the failure of constructing A...

It never ends!
Pavex will detect this scenario and return an error during code generation, so that you don't end up in an infinite loop at runtime.

Sync or async?

Error observers can be either synchronous or asynchronous.
Check out the "Sync vs async" guide for more details on the differences between the two and how to choose the right one for your use case.