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. |
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),
}