Validating & Handling Errors

Check a workflow is wired correctly, and handle what orchestrate() returns.

See Workflows for defining states and building a workflow. This page covers validation and runtime error handling.


Workflow Validation

Before orchestrating a workflow, you can validate its configuration to catch common mistakes early. Cano provides two validation methods that check for different categories of problems.

validate()

Checks the overall workflow structure. Returns CanoError::Configuration if problems are found.

Checks performed

No handlers registered — the workflow has no states mapped to tasks.

No exit states defined — the workflow has no way to terminate.

validate_initial_state()

Checks that a specific initial state has a handler registered. Returns CanoError::Configuration if the given state has no registered task or split handler.

use cano::prelude::*;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum State { Start, Process, Complete }

#[derive(Clone)]
struct MyTask;
#[derive(Clone)]
struct ProcessTask;

#[task(state = State)]
impl MyTask {
    async fn run_bare(&self) -> Result<TaskResult<State>, CanoError> {
        Ok(TaskResult::Single(State::Process))
    }
}

#[task(state = State)]
impl ProcessTask {
    async fn run_bare(&self) -> Result<TaskResult<State>, CanoError> {
        Ok(TaskResult::Single(State::Complete))
    }
}

#[tokio::main]
async fn main() -> Result<(), CanoError> {
    let store = MemoryStore::new();
    let workflow = Workflow::new(Resources::new().insert("store", store.clone()))
        .register(State::Start, MyTask)
        .register(State::Process, ProcessTask)
        .add_exit_state(State::Complete);

    // Validate structure: ensures handlers and exit states exist
    workflow.validate()?;

    // Validate that the initial state has a handler
    workflow.validate_initial_state(&State::Start)?;

    // Safe to orchestrate
    let _result = workflow.orchestrate(State::Start).await?;
    Ok(())
}

Runnable example: cargo run --example workflow_validation — a well-formed workflow that passes, plus the failure cases (a missing transition target, an unregistered initial state) and the exact errors validate() / validate_initial_state() return.


Error Handling

The orchestrate() method can return several error variants depending on what goes wrong during execution. Understanding these errors helps you build robust error recovery logic.

Error Variant Condition How to Fix
CanoError::Workflow No handler registered for current state Register a task for every reachable state with register()
CanoError::Workflow Single task returned TaskResult::Split Use register_split() instead of register() for parallel tasks
CanoError::WorkflowTimeout Wall-clock budget set via Workflow::with_total_timeout() elapsed; in-flight task aborted, compensation stack drained. Surfaced under CanoError::WithStateContext. Increase with_total_timeout() or speed up the workflow; see Resilience → Workflow Total Timeout
CanoError::Workflow Legacy with_timeout() outer tokio::time::timeout elapsed (no graceful compensation) Prefer with_total_timeout() for new code; otherwise increase with_timeout() or optimize task execution time
CanoError::Configuration PartialTimeout strategy used without timeout configured Add .with_timeout(duration) to JoinConfig
CanoError::Timeout Per-attempt timeout from TaskConfig::attempt_timeout elapsed Increase with_attempt_timeout() or speed up the task; combine with a RetryMode if transient
CanoError::RetryExhausted All retry attempts exhausted by a Task Increase retry count or fix the underlying transient failure
CanoError::CircuitOpen Call rejected by an open CircuitBreaker attached to TaskConfig Wait for the breaker's reset_timeout or fix the upstream dependency; the retry loop short-circuits — no attempts are consumed
CanoError::TaskExecution Single task panicked (message is prefixed with "panic:") Inspect the panic payload in the message; fix the underlying invariant in the task body
CanoError::* Any error propagated from task execution Check the specific task logic — TaskExecution, Store, etc.
Panic safety

Single-task execution is wrapped in catch_unwind: a panicking task surfaces as CanoError::TaskExecution("panic: …") rather than aborting the workflow. Split tasks are already isolated by tokio::task::JoinSet, so panics there propagate as task failures through the join strategy.

match workflow.orchestrate(State::Start).await {
    Ok(final_state) => println!("Completed: {:?}", final_state),
    Err(CanoError::Workflow(msg)) => eprintln!("Workflow error: {}", msg),
    Err(CanoError::Configuration(msg)) => eprintln!("Config error: {}", msg),
    Err(CanoError::Timeout(msg)) => eprintln!("Attempt timed out: {}", msg),
    Err(CanoError::RetryExhausted(msg)) => eprintln!("Retries exhausted: {}", msg),
    Err(e) => eprintln!("Task error: {}", e),
}