# Error handling & exception handlers

> **CocoIndex v1, Rust SDK (experimental).** This page documents the CocoIndex **v1** Rust SDK (the `cocoindex` crate). The Rust SDK is experimental: any release may change or remove its API, and it is not yet published on crates.io. The Python SDK pages are at https://cocoindex.io/docs/.
>
> Source: https://cocoindex.io/docs/rust/advanced_topics/exception_handlers/ · Rust docs index: https://cocoindex.io/docs/rust/llms.txt · Rust SDK tour: https://github.com/cocoindex-io/cocoindex/blob/main/rust/sdk/SHOWCASE.md · Rust examples: https://github.com/cocoindex-io/cocoindex/tree/main/examples/rust

CocoIndex isolates component failures and provides APIs for observing errors, handling interrupted updates, and reacting to failures in production.

For a quick overview of failure isolation and the two-phase model, see [What happens when a component fails](/docs/rust/programming_guide/processing_component/#what-happens-when-a-component-fails) in the Processing Component guide.

## The guiding principle

**Whether a CocoIndex call returns an error on failure depends on what's on the critical path for the call to finish and return.** Things on that path that fail surface as errors; things off that path don't.

| API | What it takes to return | Returns an error on failure? |
|---|---|---|
| `ctx.mount` / `mount!`, `ctx.mount_each` / `mount_each!`, `LiveComponentOperator::start_update` / `start_delete` | the work is *scheduled* (returns a handle) | **No** — execution runs in the background |
| `handle.ready().await` (any of the above) | reaches *ready state* | **No by default** — logged at `ERROR`; opt into propagation by installing a handler that returns `Err` |
| `LiveComponentOperator::update_full`, `update`, `delete` | reaches *ready state* of the full cycle or the child | **No by default** — same shape as `handle.ready()` |
| `ctx.scope` / `use_mount!`, `ctx.use_mount_each` / `use_mount_each!` | the inner component's *value* is produced | **Yes** — component success is on the critical path |
| `app.drop_state().await` (and `cocoindex drop`) | GC succeeds at *every* level | **Yes**, at any depth — leaking target state would otherwise go silent |

**Why `ready`/`update_full` tolerate errors by default:** `handle.ready()` is usually not awaited (callers spawn many mounts in a loop and rely on the parent waiting for its children), so an error returned to nobody would silently disappear. Logging the failure gives operators a signal regardless. The same applies to `update_full` — a periodic refresh that died on the first cycle failure would be worse than one that logs and retries on the next tick.

**Why `app.drop_state` fails at every level:** clearing tracking records is only safe if every component's delete actually ran its sink calls. If a descendant delete silently fails, `app.drop_state()` would return `Ok` while target state is still live in your database — a leak. So failures at any depth surface as an error.

## How to opt into propagation

Install an exception handler (described below). A handler that returns **`Ok(())`** swallows; a handler that returns **`Err(_)`** propagates through `handle.ready()` (or `update_full()`) up to your awaiting code. For `app.drop_state`, propagation is built in — you don't need to install anything.

[Live components](/docs/rust/advanced_topics/live_component/) follow the same principle: their `update_full`, `update` and `delete` calls route failures through this chain, with `mount_kind` set to `UpdateFull`, `Update` or `Delete`.

The Rust `LiveComponentOperator` has no `report_exception`: return the error from `process_live` instead.

## Processing and submit phases

CocoIndex processes each component in two phases:

1. **Processing** — runs your function, declares target states in memory. This phase is side-effect-free. If it fails (e.g., a parsing error, an API timeout), no writes were attempted.
2. **Submit** — writes changes to target backends. This phase only runs after processing completes successfully.

This separation means a processing failure never leaves partial data in your targets.

## Interrupted updates and recovery

An update can be interrupted by various events: a process kill (SIGKILL), Ctrl+C (SIGINT), an unhandled error, or a target backend failure during submit.

**What state is left behind?**

CocoIndex's internal database (LMDB) uses transactions, so its own state is always consistent even after a crash. CocoIndex tracks all possible states a target could be in — if an update is interrupted partway through a commit, both the old and new states are retained as possibilities. This ensures no state is ever lost.

**Recovery is automatic.** On the next app update, CocoIndex computes the current desired state and reconciles against all possible previous states. The target connector converges the target to the correct state regardless of whether the previous commit partially succeeded or never ran.

For details on how target handlers deal with multiple possible previous states after an interruption, see [Custom Target Connector — Handle multiple previous states](/docs/rust/advanced_topics/custom_target_connector/#handle-multiple-previous-states).

## Monitoring errors

`app.start_update(...)` returns an `UpdateHandle` that exposes processing stats, including error counts:

```rust
let mut handle = app.start_update(|ctx| async move {
    // ...
    Ok(())
})?;

// Poll stats at any time
let detailed = handle.detailed_stats_snapshot();
println!("Errors: {}", detailed.total().num_errors);

// Follow progress until the update finishes
loop {
    if handle.changed().await?.is_done() {
        break;
    }
    println!("{} errors so far", handle.detailed_stats_snapshot().total().num_errors);
}
let result = handle.result().await?;
```

See [Progress monitoring](/docs/rust/advanced_topics/progress_monitoring/) for the full `UpdateHandle` API.

## Exception handlers

For background-mounted components (`ctx.mount` / `mount!`, `ctx.mount_each` / `mount_each!`, live components and `ctx.auto_refresh`), you can register **exception handlers** to observe or react to failures — for example, to send alerts, record metrics, or implement custom logic.

CocoIndex supports two levels of exception handlers:

- **Global (environment-level)**: registered once with `EnvironmentBuilder::exception_handler`, usually in your lifespan function; applies to all background mounts in the environment.
- **Scoped**: `ctx.with_exception_handler(handler)` returns a scoped `Ctx`; the handler applies to the background mounts made from that `Ctx`. `ctx.mount_live_with_handler` attaches a handler to one live component.

A handler is `Fn(&Error, &ExceptionContext) -> Result<()>`: return `Ok(())` to swallow the failure, `Err(_)` to propagate it.

**Note**
Exception handlers only apply to background mounts. `ctx.scope` / `use_mount!` and `use_mount_each!` return the child's error directly to the caller, since the parent has an explicit dependency on the result.

### Global exception handler

Register a handler inside your `#[cocoindex::lifespan]` function using `builder.exception_handler(...)`:

```rust
#[cocoindex::lifespan]
async fn lifespan(builder: EnvironmentBuilder) -> Result<EnvironmentBuilder> {
    Ok(builder.exception_handler(|err, exc| {
        println!("[{}] {:?} failed at {}: {err}", exc.env_name, exc.mount_kind, exc.stable_path);
        Ok(())
    }))
}
```

The same method works on an `Environment::builder()` you build yourself.

This replaces the default "log error" behavior for all background mounts in the environment.

### Scoped exception handler

Use `ctx.with_exception_handler(handler)` to get a `Ctx` whose background mounts go through the handler first:

```rust
#[cocoindex::function]
async fn process_all(ctx: &Ctx, files: Vec<FileEntry>) -> Result<()> {
    let scoped = ctx.with_exception_handler(|err, exc| {
        println!("Failed processing {}: {err}", exc.stable_path);
        Ok(())
    });
    for file in files {
        let key = file.relative_path().display().to_string();
        scoped
            .mount(&key, move |child| async move { process_file(&child, file).await })
            .await?;
    }
    Ok(())
}
```

Only mounts made from `scoped` see the handler. A mount made from the unscoped `ctx` goes to the global handler.

The handler applies to every background mount made from the scoped `Ctx` (or a `Ctx` derived from it, for example with `with_subpath`), including every item of a `mount_each` and every child of a live component mounted from it.

A child component's `Ctx` inherits the handler chain of the `Ctx` it was mounted from, so a handler installed with `ctx.with_exception_handler` also sees the failures of the mounts that child makes itself, at any depth. In Python, a scoped handler does not reach the mounts those components make themselves. The global handler is the outermost entry of every chain.

### Handler type

The type alias is:

```rust
pub type ExceptionHandler =
    Arc<dyn Fn(&Error, &ExceptionContext) -> Result<()> + Send + Sync + 'static>;
```

`EnvironmentBuilder::exception_handler`, `ctx.with_exception_handler` and `ctx.mount_live_with_handler` accept any closure with that signature and wrap it.

Rust handlers are synchronous closures; Python also accepts async handlers.

### What `err` is

Handlers receive the same `Error` that a foreground call such as `ctx.scope(...)` would return for the same failure. A failure your own code returned with `Error::user(..)` arrives as `Error::User(inner)`; `inner.downcast_ref::<T>()` recovers its type. `Error::DeadlineExceeded` is the cooperative deadline. An error a handler returns keeps its value as well: `handle.ready()` returns exactly what the last handler in the chain returned.

```rust
// The handler wraps the component's error in its own error type and returns it.
let app = Environment::builder()
    .db_path(db_path)
    .exception_handler(|err, _exc| Err(Error::user(DeadLetter(err.to_string()))))
    .build()
    .await?
    .app("my_app")
    .await?;

// `handle.ready()` returns the same value the last handler returned.
let handle = ctx
    .mount(&"child", |_child| async move { Err(Error::engine("boom")) })
    .await?;
if let Err(e) = handle.ready().await {
    let is_dead_letter = match &e {
        Error::User(inner) => inner.downcast_ref::<DeadLetter>().is_some(),
        _ => false,
    };
    // ...
}
```

### `ExceptionContext` fields

Your handler receives an `ExceptionContext` struct with information about the failure:

| Field | Type | Notes |
|---|---|---|
| `env_name` | `String` | Name of the app the failing component runs under |
| `stable_path` | `String` | Stable path of the failing component |
| `parent_stable_path` | `Option<String>` | Set for a child mount or delete |
| `processor_name` | `Option<String>` | Name of the processor that failed, if known |
| `mount_kind` | `MountKind` | `Mount` (`ctx.mount` / `mount!`, `ctx.mount_each` / `mount_each!`), `UpdateFull` (a live component's full pass, or an `auto_refresh` cycle), `Update` or `Delete` (the live component operator) |
| `is_background` | `bool` | `true` for a background mount; for a live component, `true` once it is ready and running in live mode |
| `source` | `ExceptionSource` | `Component` for the component's own error; `Handler` for an error a nearer handler returned |

There is no `original_exception` field in Rust. The error a handler returns replaces the component's error for the next handler.

### Handler stacking and fallback

Handlers are stacked: the most specific (innermost) handler runs first.

If the innermost handler returns `Err`, the next outer handler is called with that new error. In this case `exc.source` is `ExceptionSource::Handler`.

This continues up the stack. If every handler returns `Err`, the last handler's error propagates through `handle.ready()` (see [How to opt into propagation](#how-to-opt-into-propagation)). If no handler is registered, CocoIndex falls back to the built-in behavior: logging the error at `ERROR` level, with no crash.

```rust
#[cocoindex::lifespan]
async fn lifespan(builder: EnvironmentBuilder) -> Result<EnvironmentBuilder> {
    Ok(builder.db_path("...").exception_handler(|err, exc| {
        match exc.source {
            // A handler itself failed: `err` is the error that handler returned.
            ExceptionSource::Handler => println!("Handler error: {err}"),
            ExceptionSource::Component => println!("Component error: {err}"),
        }
        Ok(())
    }))
}

app.update(|ctx| async move {
    let scoped = ctx.with_exception_handler(|err, _exc| {
        println!("inner: {err}");
        Err(Error::engine("handler failed")) // falls through to the global handler
    });
    scoped
        .mount(&"child", |_child| async move { Err(Error::engine("boom")) })
        .await?;
    Ok(())
})
.await?;
```

Users who never register handlers see identical behavior to the default — errors from background mounts are logged at `ERROR` level and siblings continue unaffected.
