Error Handling
Retries are declared per-task (or per-asset) via decorator kwargs. Rust owns the retry loop – you do not implement retries in Python.
Declaring retries
Section titled “Declaring retries”from barca import task, asset
@task(retries=3, retry_backoff=1.0)def deploy_model(model: dict) -> None: upload_to_endpoint(model)retries– total number of attempts.retries=3means up to 3 attempts (1 initial + 2 retries). Default is 1 (no retry).retry_backoff– base backoff in seconds. Delay before attempt N isretry_backoff * (N-1)– no delay before the first attempt. Default is 0.
With retries=3, retry_backoff=1.0:
| Attempt | Delay before |
|---|---|
| 1 | 0s (immediate) |
| 2 | 1.0s |
| 3 | 2.0s |
Retries work on assets too:
@asset(retries=2, retry_backoff=0.5)def flaky_api_data() -> dict: return call_unreliable_api()How the retry loop works
Section titled “How the retry loop works”Rust’s coordinator owns retries entirely:
- The worker process runs the function.
- If the function raises an exception, the worker reports failure back to Rust.
- Rust waits for the backoff delay, then spawns a new worker attempt.
- If all attempts are exhausted, the step is marked as failed.
Each attempt is a fresh worker invocation. There is no shared state between attempts.
Failure propagation
Section titled “Failure propagation”Retries do not cascade. Each node in the DAG owns its own retry boundary.
@task(retries=3, retry_backoff=1.0)def deploy(model: dict) -> dict: return upload(model) # retried up to 3 times
@task(inputs={"d": deploy}, retries=2)def notify(d: dict) -> None: send_slack(f"deployed {d}") # retried up to 2 times, independentlyIf deploy exhausts its 3 attempts and fails:
deployis marked failed.notifyis not attempted – its input dependency failed.notify’s own retries are irrelevant because it never started.
If deploy succeeds but notify fails:
notifyretries up to 2 times using its own retry policy.deployis not re-run – it already succeeded.
Parent retries never re-run child tasks, and child retries never trigger parent retries.
Common mistakes
Section titled “Common mistakes”Implementing retry loops in Python
Section titled “Implementing retry loops in Python”# Wrong -- Rust already handles this.@task()def deploy(model: dict) -> None: for attempt in range(3): try: upload(model) return except Exception: time.sleep(attempt * 1.0) raise RuntimeError("gave up")This defeats Rust’s retry tracking. The coordinator sees one attempt that either succeeds or fails after internal looping. Use retries=3, retry_backoff=1.0 on the decorator instead.
Expecting parent retries to cover sub-task failures
Section titled “Expecting parent retries to cover sub-task failures”@task(retries=5)def release(model: dict) -> None: deploy(model) # if deploy is a @task, calling it directly bypasses barca notify()Calling @task functions directly is just a regular Python call – barca does not manage it. If deploy is a separate @task, wire it through inputs= so the coordinator can retry it independently.
Catching all exceptions to prevent retries
Section titled “Catching all exceptions to prevent retries”# Wrong -- swallowing errors prevents Rust from retrying.@task(retries=3)def deploy(model: dict) -> None: try: upload(model) except Exception: log.error("failed") return # silently succeeds -- no retry happensIf you want retries to work, let the exception propagate. Catch only exceptions you genuinely want to handle without retrying.