Skip to content

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.

  • Choose an error strategy: fail_fast vs continue.
  • Send rejected records to a dead-letter queue so good data still flows.
  • Read Clinker’s exit codes to know what happened.

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.
  • continue keeps 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.
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.

This pipeline keeps going past bad rows. Predict the outcome, then run it.

pipeline.yaml // editable

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.

When clinker run finishes, its exit code tells you the outcome at a glance:

CodeMeaning
0success, no rejected rows
1config, schema, or CXL compile error (a bad type, a missing field); the job never started
2completed, but some rows went to the DLQ
3a CXL evaluation error, or the DLQ rate ceiling was hit; this is where a fail_fast stop on a bad row lands
4an I/O, format, or spill error
130interrupted (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?

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-run

Predict: 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.