Errors & the dead-letter queue
The question for this lesson: your file has 10,000 rows and three of them are malformed. Should the whole job stop, or should it process the 9,997 good rows and set the bad ones aside? You decide. Here’s how.
What you’ll be able to do
Section titled “What you’ll be able to do”- Choose an error strategy:
fail_fastvscontinue. - Send rejected records to a dead-letter queue so good data still flows.
- Read Clinker’s exit codes to know what happened.
Two strategies
Section titled “Two strategies”The pipeline-level error_handling: block sets the strategy:
fail_fast(the default) stops on the first bad record. Best while you’re developing, or when any bad row means the whole batch is untrustworthy.continuekeeps going and sends each bad record to the dead-letter queue. Best for large feeds where a few bad rows are expected and you want the good data now.
Configuring the dead-letter queue
Section titled “Configuring the dead-letter queue”error_handling: strategy: continue dlq: path: ./output/errors.csv # rejected records land here include_reason: true # add a column explaining why each was rejected include_source_row: true # keep the original row for debugging🌱 New here? — why keep the bad rows?
A dead-letter queue is a to-do list, not a trash can. With include_reason and
include_source_row, each rejected row arrives with what failed and the original data,
so someone can fix the source and re-run just those rows. Discarding bad rows silently hides
data-quality problems; the DLQ surfaces them.
Use: continue + DLQ
Section titled “Use: continue + DLQ”This pipeline keeps going past bad rows. Predict the outcome, then run it.
> output appears here — predict, then run
Modify: switch the strategy to fail_fast
Section titled “Modify: switch the strategy to fail_fast”Same five orders, same one bad row, but now flip the dial. In the runner above, find
strategy: continue in the error_handling: block and change it to strategy: fail_fast.
Leave everything else as-is, then predict what changes before you run it again.
Exit codes: what happened?
Section titled “Exit codes: what happened?”When clinker run finishes, its exit code tells you the outcome at a glance:
| Code | Meaning |
|---|---|
0 | success, no rejected rows |
1 | config, schema, or CXL compile error (a bad type, a missing field); the job never started |
2 | completed, but some rows went to the DLQ |
3 | a CXL evaluation error, or the DLQ rate ceiling was hit; this is where a fail_fast stop on a bad row lands |
4 | an I/O, format, or spill error |
130 | interrupted (Ctrl-C) |
Watch the line between 1 and 3. Code 1 means the pipeline couldn’t even be built:
a type that doesn’t exist, a field you forgot. Code 3 means it built fine and started
running, then a row’s data broke an expression. Under fail_fast, the first bad row stops
the job and you get exit 3 (not 1). Exit 2 is the one to watch in automation: the job
succeeded, but rows were rejected, so check the DLQ.
// quick check
Under `strategy: continue`, a row fails to convert `amount` to a number. What happens?
continue routes the bad row to the dead-letter queue (with include_reason, you get why) and keeps going. fail_fast would have stopped instead.
Create: an error policy from scratch
Section titled “Create: an error policy from scratch”Now author the policy yourself. You’re building the nightly invoices pipeline: a few malformed
rows are expected, they must be quarantined for review (never silently dropped), and the job
must still finish so the downstream reports run. Write the error_handling block, then predict
the exit code.
error_handling: strategy: continue # don't abort on a bad row... dlq: path: ./output/invoice_errors.csv include_reason: true # ...record WHY each row was rejected... include_source_row: true # ...and keep the original row to fix and re-runPredict: a run processes 500 invoices and 2 fail to coerce. How many rows reach the output, how
many reach invoice_errors.csv, and what exit code does clinker run return?
498 rows reach the output, 2 reach invoice_errors.csv, and the exit code is 2:
completed, but some rows went to the DLQ. Exit 2 is the signal automation watches: the job
succeeded, but rows need attention. (Had you chosen fail_fast, the first bad invoice would abort
with exit 3 and write no DLQ file, which is wrong for this job.) Check your config first with
clinker run invoices.yaml --dry-run, and the plan with --explain, before the real run.