Skip to content

Option & Result

Two questions come up constantly in a data engine: is this field even here? and did this step fail? Many languages answer the first with null and the second by throwing. Rust answers both with ordinary types you can’t ignore — Option and Result — and the engine’s source is full of them.

You’ll be able to: read Option and Result in real signatures, and explain how the engine uses them to separate “no more records” and “absent field” from “this failed.”

You already saw one: resolve(&self, name: &str) -> Option<&Value>. The Option is honest about a fact the type system would otherwise hide — a field you ask for might not exist. Some(&value) if it’s there, None if it isn’t. There’s no null to forget to check; to get at the value you must handle the None.

The same shape marks the end of a finite source. A reader’s next_record yields Some(record) for each row and None when the file is exhausted — that None is how a finite job knows to stop:

clinker-format ·traits.rs ·next_record fn @47d2e12

But reading a record can also fail — a malformed line, a broken encoding. That’s not “no more records”; it’s an error. So next_record doesn’t just return an Option — it returns a Result wrapping one:

fn next_record(&mut self) -> Result<Option<Record>, FormatError>;
// Ok(Some(record)) → a row
// Ok(None) → end of input (clean)
// Err(e) → something went wrong

Result makes failure a value you handle, not an exception that unwinds the stack. Three outcomes, all in the type: a row, a clean end, or an error.

Not every error should kill a job. If one row in a million can’t be coerced — "active".to_int() is nonsense — Clinker can route that single bad record to the dead-letter queue (the dlq count you saw in --dry-run) and keep going, instead of aborting the whole run. The coercion failure itself is a typed error:

clinker-record ·coercion.rs ·CoercionError type @47d2e12
pub enum CoercionError {
TypeMismatch { from: &'static str, to: &'static str, value: String },
ParseFailure { input: String, target: &'static str },
}

That’s the difference the type system helps enforce: a data error (one bad row) is recoverable and can be DLQ’d; an invariant error (a bug, an impossible state) is fatal. You’ll meet the engine’s full error vocabulary, PipelineError, in Phase 3.

rust // editable

"15200" coerces; "active" doesn’t, and the Err arm is exactly where the engine decides “DLQ this row and continue.”

// quick check

A reader's next_record returns Result<Option<Record>>. What does Ok(None) mean?

You can read absence and failure in the types. Next: the small struct that rides along with every record so the engine always knows where a row came from.