Lesson 5: Error Handling and Retry Strategies
Learning goals:
- Tell transient errors from permanent ones
- Master retry strategies and backoff algorithms
- Learn to design compensating actions and rollback mechanisms
Prerequisites: Lesson 4: State Management and Passing Context | Next: Lesson 6 >>
Errors Are the Norm in Workflows
Your workflow runs perfectly ten times. On the eleventh, at step 8, the API returns a 503. The workflow crashes.
So you add a try-catch, catch the error, log it, and keep going. On the twelfth run, the database connection times out. The workflow continues, but the write failed, and now your data is inconsistent.
Error handling isn't as simple as "add a try-catch."
In a workflow, error handling has to answer three questions:1
- Is this error temporary or permanent? (network jitter vs missing permissions)
- Should you retry, skip, or abort? (a retry might fix it vs a retry makes it worse)
- If you abort, how do you clean up the steps that already finished? (roll back the database vs send a cancellation notice)
If the step that failed is optional (say, sending a notification), skip it and move on. Letting a non-critical failure not derail the whole run is called graceful degradation. But if the failed step is critical, skipping it leaves an inconsistent state, so you should abort instead.
Without answers, your workflow is either too fragile (one small error takes it down) or too dangerous (it ignores errors and keeps running, leaving inconsistent state behind).2
Classifying Errors: Transient vs Permanent
Transient errors are temporary; a retry might succeed.3
Common transient errors:
- Network timeouts
- Service temporarily unavailable (503 Service Unavailable)
- Rate limiting (429 Too Many Requests)
- Database connection pool exhausted
- Temporary lock conflicts
What they share: they usually come from resource contention, network fluctuation, or temporary overload, and waiting a moment before retrying tends to work.
Permanent errors won't succeed on retry; they need a code or config fix.2
Common permanent errors:
- Missing permissions (401 Unauthorized, 403 Forbidden)
- Resource not found (404 Not Found)
- Malformed input (400 Bad Request)
- Business-logic errors (insufficient balance, zero stock)
- Code bugs (null pointer, divide by zero)
What they share: they come from misconfiguration, code bugs, or violated business rules, and retrying just wastes resources.
How to tell them apart:
Retry Strategies
For transient errors, retrying is your first move. But there's more to retrying than meets the eye.3
Strategy 1: Fixed-delay retry
The problem: if the errors come from an overloaded service, every client retrying at once makes the overload worse (the thundering-herd effect).
Strategy 2: Exponential backoff
The benefit: each retry doubles the interval, giving the service more time to recover instead of hammering it.3
Strategy 3: Exponential backoff + jitter
The benefit: jitter keeps multiple clients from retrying at the exact same instant, spreading the load.3
This is the recommended strategy for production.4
Strategy 4: Selective retry
The key idea: only retry transient errors. Throw permanent errors immediately so you don't burn cycles on pointless retries.5
The Circuit Breaker Pattern
The problem: if a service keeps failing (say, a crashed database) and every request retries three times, you burn resources for nothing and drag down the whole workflow. And if that service is a dependency of other services, the failure propagates down the chain into a cascading failure.
A circuit breaker: when the error rate crosses a threshold, it temporarily stops calling the failing service and fails fast instead, avoiding wasted resources.1
Three states
Closed: working normally. Requests pass through, and the breaker tracks the error rate.
Open: the service is considered unavailable. Requests fail fast without calling it.
Half-open: after a timeout, a few trial requests go through. If they succeed, the breaker returns to closed; otherwise it stays open.
Implementation
When to use it: calls to external services, databases, file systems, and other dependencies that can fail in bulk.1
Compensating Actions and Rollback
The problem: the workflow did three writes (write to the database, send an email, update the cache), and step 4 failed. How do you undo the first three?2
Pattern 1: Transactional operations
When it fits: every operation lives in the same database that supports transactions.
The limit: it can't span systems (say, database + file system + API call).
Pattern 2: Compensating actions (the Saga pattern)
The idea: define a compensating action for each operation, and on failure run the compensations to undo the steps that already completed.4
Key points:
- Every step has a
forward (the action) and a compensate (the undo).
- On failure, run the compensations for completed steps in reverse order.
- A compensation can itself fail; log it and flag it for a human.4
Pattern 3: Idempotent design
Idempotent: running it N times has the same effect as running it once.2
The benefit: if a step runs twice because of a network hiccup (the first attempt timed out but actually succeeded), idempotency guarantees no duplicate side effects.2
Layers of Error Handling
A good workflow handles errors at three layers:
Layer 1: The individual operation
Layer 2: The workflow step
This layer writes every failure into workflowState.errors with the step name, error message, and timestamp. That's your error log, and when you're debugging you lean on this record rather than your memory.
Layer 3: The whole workflow
Three layers of protection: retry at the operation layer, record at the step layer, recover and notify at the workflow layer.
Next: Lesson 6: Real-World Workflows in Practice — Put it all together to build three production-grade workflows: code refactoring, documentation generation, and test automation