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

Errors

Vyuh uses different error shapes for different jobs. Application handlers should usually return vyuh::Error. Subsystems still keep their own error types for framework machinery, and rendered output is transport-specific.

Mental Model

LayerTypeUse
Application errorvyuh::Errornormal handler failure from routes, commands, tasks, signals, and emitters
Subsystem errorCommandError, TaskError, SignalError, EmitterError, SiteErrorparsing, registration, storage, dispatch, startup, and other framework machinery
Render inputErrorViewtransport-neutral error data passed to JSON, HTML, and command renderers
HTTP JSON bodyErrorReportdefault JSON response body for routes and middleware
CLI outputcommand rendererhuman-readable stderr output for command failures

The important boundary is this: users return Error from handler logic, while Vyuh converts that error into ErrorView before rendering. ErrorReport is the default HTTP JSON body, not the universal error abstraction.

Rendering Pipeline

All rendered errors follow the same normalization path:

vyuh::Error / ValidationReport / subsystem error
        -> ErrorView
        -> JSON renderer / HTML renderer / command renderer

ErrorView is the shared renderer input:

#![allow(unused)]
fn main() {
pub struct ErrorView {
    pub status: StatusCode,
    pub source: ErrorSourceKind,
    pub kind: ErrorKind,
    pub code: Cow<'static, str>,
    pub message: Cow<'static, str>,
    pub errors: Option<serde_json::Value>,
    pub validation: Option<ValidationReport>,
}
}

Use ErrorView when deciding what message to show. Use ErrorReport only when you want Vyuh’s default HTTP JSON envelope.

Application Errors

Use vyuh::Error for ordinary handler failures:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

async fn show_user(Data(input): Data<UserLookup>) -> Result<Json<UserOut>, Error> {
    let user = find_user(input.id)
        .await
        .ok_or_else(|| Error::not_found("user not found"))?;
    Ok(Json(user))
}
}

Convenience constructors cover common cases:

#![allow(unused)]
fn main() {
Error::bad_request("invalid input")
Error::not_found("record not found")
Error::invalid("business rule failed")
Error::conflict("version conflict")
Error::unavailable("upstream unavailable")
Error::other(err)
Error::wrap(ErrorKind::Unavailable, err)
}

Use with_context(...) to add operator-facing detail while preserving the machine-readable kind.

Subsystem Errors

Subsystem errors describe framework mechanics:

  • CommandError: unknown commands, unknown flags, help rendering, unsupported command schemas, command argument parsing, and command rendering.
  • TaskError: task store, lease, migration, serialization, and retry machinery.
  • SignalError and EmitterError: registration, dispatch, source setup, and source execution machinery.
  • SiteError: configuration, build, startup, and shutdown lifecycle failures.

Application code inside those handlers should still return Error:

#![allow(unused)]
fn main() {
async fn command(Data(args): Data<ReindexArgs>) -> Result<(), vyuh::Error> {
    reindex(args.full).await.map_err(Error::other)
}
}

Validation

Validation failures are structured reports. Valid<E> runs Validate after the inner data wrapper parses or extracts successfully:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

async fn create(Valid(Data(input)): Valid<Data<CreateUser>>) -> Result<(), Error> {
    Ok(())
}
}

Routes convert validation failures into a 422 ErrorView, then render the view as JSON, HTML, or any custom HTTP response. Commands render the same field-oriented report as CLI output:

Validation failed for command 'create-user':

  --email
    Enter a valid email address.

Use 'create-user --help' for usage.

See Validation for the full code, message, and params error object.

HTTP Errors

Routes, middleware, extractors, auth, database, template, validation, and application failures normalize into ErrorView before a response is rendered. Multipart upload parse, MIME screening, and size-limit failures use the same pipeline. The default JSON renderer turns that view into ErrorReport.

Upload-specific status codes follow the same model: malformed multipart returns 400, unsupported declared or sniffed file type returns 415, oversized uploads return 413, and upload validation returns 422.

Applications can customize JSON and HTML rendering separately:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;
use vyuh::errors::{ErrorConf, HttpErrorRenderMode};

let conf = SiteConf::default().errors(
    ErrorConf::default()
        .json(|ctx, view| async move {
            (
                view.status,
                Json(serde_json::json!({
                    "code": view.code,
                    "message": "Custom JSON message",
                    "path": ctx.path,
                    "errors": view.errors,
                })),
            )
                .into_response()
        })
        .html(|ctx, view| async move {
            (
                view.status,
                Html(format!("<h1>{}</h1><p>{}</p>", view.status, view.message)),
            )
                .into_response()
        })
        .http_mode(HttpErrorRenderMode::Auto),
);
}

HttpErrorRenderMode::Auto uses JSON by default and HTML when the request accepts text/html. Use Json or Html to force one renderer for all HTTP errors.

Renderer inputs are request-aware. JSON and HTML renderers receive ErrorRequestContext, which includes method, URI, path, and headers:

#![allow(unused)]
fn main() {
ErrorConf::default().json(|ctx, view| async move {
    (
        view.status,
        Json(serde_json::json!({
            "code": view.code,
            "message": view.message,
            "path": ctx.path,
        })),
    )
        .into_response()
})
}

The lower-level handler(|ctx, report| ...) hook remains available when an application wants to replace the final HTTP response from the default ErrorReport directly.

Command Errors

Site::run builds the site, runs a command when one is supplied, and renders command failures for a terminal. With no command arguments it runs the built-in serve command:

  • help output goes to stdout and succeeds;
  • unknown command, unknown flag, missing argument, parse failure, validation failure, and handler failure are rendered to stderr and return non-zero;
  • handler Error values are normalized into ErrorView.

Command parsing and help generation remain CommandError concerns. Handler logic should return Error.

Customize command output separately from HTTP rendering:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;
use vyuh::errors::ErrorConf;

let conf = SiteConf::default().errors(
    ErrorConf::default().command(|ctx, view| {
        if view.validation.is_some() {
            format!("{} failed validation. Run '{} --help'.", ctx.command, ctx.command)
        } else {
            format!("{} failed: {}", ctx.command, view.message)
        }
    }),
);
}

Command renderers receive ErrorCommandContext, which includes the command name and raw command arguments. They return a string that is written to stderr for failures.

Commands do not render ErrorReport; command output is terminal text.

Task Errors And Retry

Task retry is explicit. A task handler returning Err(Error) marks the task as failed terminally. Vyuh does not infer retry behavior from ErrorKind.

Return TaskState::retry(...) when work should be retried:

#![allow(unused)]
fn main() {
use vyuh::prelude::*;

async fn send_email(Data(job): Data<EmailJob>) -> Result<TaskState<String>, Error> {
    match deliver(&job).await {
        Ok(()) => Ok(TaskState::complete("sent".to_string())?),
        Err(err) if err.is_transient() => Ok(TaskState::retry(
            Some(std::time::Duration::from_secs(60)),
            err.to_string(),
        )),
        Err(err) => Err(Error::unavailable(err.to_string())),
    }
}
}

This keeps retry policy visible in task code instead of hiding it in a broad error classification.

ErrorKind Mapping

ErrorKind carries the broad class of failure. HTTP routes map it to status codes when converting into ErrorReport:

KindHTTP Status
BadRequest400
Unauthorized401
Forbidden403
NotFound404
Conflict409
Integrity409
Invalid422
RateLimited429
Unavailable503
Other500

Commands do not render ErrorReport; they render terminal text. Tasks do not retry from ErrorKind; they use TaskState::retry(...).