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.

use pavex::error_observer;
use tracing_log_error::log_error;

#[error_observer]
pub async fn emit_error_log(e: &pavex::Error) {
    log_error!(e, "An error occurred during request processing");
}

Signature

Pavex accepts a wide range of function signatures for error observers, as long as they satisfy these requirements:

  1. One input parameter is a &pavex::Error1.
  2. They don't return a value2.

Other than that, you have a lot of freedom in how you define your error handlers:

Defining error observers

Use the #[error_observer] attribute to define a new error observer:

use pavex::error_observer;
use tracing_log_error::log_error;

#[error_observer]
pub async fn emit_error_log(e: &pavex::Error) {
    log_error!(e, "An error occurred during request processing");
}

The signature of this error observer satisfies all the requirements listed in the previous section.

Registration

Invoke Blueprint::error_observer to register error observers for your application:

use crate::logger::EMIT_ERROR_LOG;
use pavex::Blueprint;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.error_observer(EMIT_ERROR_LOG); // (1)!
    // [...]
}
  1. EMIT_ERROR_LOG is a strongly-typed constant generated by the #[error_observer] attribute on the emit_error_log function.
    Check out the documentation on component ids for more details.

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.

Position matters

An error observer will only be invoked for errors that occur in a route or middleware that was registered after the error observer itself.

use crate::{
    logger::EMIT_ERROR_LOG,
    routes::{INDEX, LOGIN},
};
use pavex::Blueprint;

pub fn blueprint() -> Blueprint {
    let mut bp = Blueprint::new();
    bp.route(INDEX); // (1)!
    bp.error_observer(EMIT_ERROR_LOG);
    bp.route(LOGIN); // (2)!
    // [...]
}
  1. emit_error_log won't be invoked for errors that occur while processing requests to the INDEX route, since it was registered before the error observer.
  2. emit_error_log will be invoked for errors that occur while processing requests to the LOGIN route, since it was registered after the error observer.

Dependency injection

Error observers can take advantage of dependency injection.

use crate::RootSpan;
use pavex::error_observer;

#[error_observer]
pub async fn enrich_root_span(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.


  1. 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

  2. Or, if you prefer, they return the unit type, ()