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 Inside effect!

The bind* operator short-circuits on failure — if a bound effect fails, the whole effect! block fails with that error. But you can also handle errors within the block.

The Default: Short-Circuit

effect! {
    let a = bind* step_a();    // if this fails → whole block fails
    let b = bind* step_b(a);   // if this fails → whole block fails
    b
}

This matches ? in Result. You get clean sequencing at the cost of aborting early. For most code, that’s exactly what you want.

Catching Errors Mid-block

To handle an error inline and continue, use .catch before the bind*:

effect! {
    let user = bind* fetch_user(id).catch(|_| succeed(User::anonymous()));
    // If fetch_user fails, we get User::anonymous() and continue
    render_user(user)
}

.catch converts a failure into a success (or a different effect). The bind* then sees a successful effect.

Converting Errors with map_error

Often you have multiple effect types with different E parameters and need to unify them:

#[derive(Debug)]
enum AppError {
    Db(DbError),
    Network(HttpError),
}

effect! {
    let user = bind* fetch_user(id).map_error(AppError::Db);
    let data = bind* fetch_external_data(user.id).map_error(AppError::Network);
    process(user, data)
}

Both effects are converted to the same AppError before binding. The block’s E parameter is AppError throughout.

Handling Errors as Values

The current Effect API does not expose a fold method. Use .catch / .catch_all, or run the effect and pattern match on Result at the boundary.

effect! {
    let outcome = bind* risky_operation()
        .map(|val| format!("Success: {val}"))
        .catch_all(|err| format!("Error: {err}"));
    log_outcome(outcome)
}

catch_all turns a typed failure into a fallback success value, so the resulting effect is infallible through E.

Re-raising Errors

Inside a .catch handler, you can inspect the error and decide whether to recover or re-fail:

effect! {
    let result = bind* db_operation().catch(|error| {
        if error.is_transient() {
            // Transient: retry once with a fallback
            fallback_db_operation()
        } else {
            // Permanent: re-raise
            fail(error)
        }
    });
    result
}

fail(error) inside a handler produces a failing effect — the outer bind* then propagates it.

Accumulating Multiple Errors

Short-circuit stops at the first error. The current root API does not include validate_all; collect independent validation errors manually at the boundary:

let mut errors = Vec::new();

if let Err(error) = run_blocking(validate_name(&input.name), ()) {
    errors.push(error);
}
if let Err(error) = run_blocking(validate_email(&input.email), ()) {
    errors.push(error);
}
if let Err(error) = run_blocking(validate_age(input.age), ()) {
    errors.push(error);
}

Chapter 8 covers accumulation patterns in detail.

The Rule of Thumb

WantDo
Stop at first failureplain bind* effect
Provide a fallback`bind* effect.catch(
Unify error typesbind* effect.map_error(Into::into)
Turn failure into value`bind* effect.catch_all(
Collect all failuresManual accumulation outside the macro