This content is for v1.1.0. Switch to the latest version for up-to-date documentation.
Error handling
Try[T] is iter.Seq2[T, error]: a lazy sequence whose elements each
either succeeded or carry an error. The design’s central choice is that
the pipeline does not decide what an error means — the consumer does,
because both policies are legitimate in the same codebase: a malformed
line in a log scan should probably be skipped, a failed row scan should
probably abort.
vals, err := t.Collect() // stop at the first error; partial slice + errorvals, errs := t.CollectAll() // drain everything, gather all errorsvals := t.Ignore().Collect() // drop failures, continue as a plain Seqerrs := t.Errs().Collect() // the other half: just the errorsThe five rules
Section titled “The five rules”Every Try operator follows the same rule set, so nothing needs looking
up per operator:
- Intermediates never inspect errored elements.
Filter’s predicate andMap’s function are simply not called on them; the errored element flows through untouched. - Counting is positional.
Take(3)takes three elements, errored or not — so it consumes at most three, which is what keeps termination reasoning intact. The spelling for “three successes” is.Ignore().Take(3). - An error never terminates
TakeWhile. Only a successful element failing the predicate ends the sequence. - Operators that generate an error yield the zero value with it.
When
err != nil, don’t read the value — that’s the one obligation on consumers. - Single-error terminals stop at the first error (
Collect,Fold,ForEach,Err,Count). OnlyCollectAlland the intermediates continue past errors.
Adding context where it exists
Section titled “Adding context where it exists”A scan pipeline five stages deep produces errors with no positional
information. WrapErr at the stage that has the position is the
difference between a debuggable error and a useless one:
rows.WrapErr(func(err error) error { return fmt.Errorf("orders row %d: %w", n, err)})Recover turns chosen errors back into values; OnError is the logging
tap; Must converts the first error into a panic carrying the error value
itself — for pipelines where failure is a programming bug.
Writing a producer that owns a resource
Section titled “Writing a producer that owns a resource”There is no Close on an iterator, and adding one would break range.
The pattern that works is lazy acquisition: open the resource inside
the iteration closure, so a pipeline that is built but never consumed
holds nothing, and defer fires even when a downstream stage stops early.
func Rows[T any](open func() (*sql.Rows, error), scan func(*sql.Rows) (T, error)) catena.Try[T] { return func(yield func(T, error) bool) { var zero T rows, err := open() if err != nil { yield(zero, err) // acquisition failure is the first element return } defer rows.Close() // runs on normal exit AND early termination for rows.Next() { v, err := scan(rows) if err != nil { v = zero } if !yield(v, err) { return // the defer fires } } if err := rows.Err(); err != nil { yield(zero, err) } }}Two properties carry the whole design, and both are tested against a real
database/sql driver in bake_test.go:
- Never consumed ⇒ never opened. Take an opener, not an open resource.
- Early termination closes. A downstream
Take(3)returns through the producer, which runs its defers — composing through any number of stages.
The same shape works for files (bufio.Scanner inside the closure — see
example_test.go), network readers, and anything else that must be
released. Cancellation enters the same way, at the edge:
s.UntilDone(ctx) passes elements through until the context is done, then
yields the context’s error and stops.
One deliberate hole to know about: Pull inverts control, and if you
never call its stop, the producer never returns and its defers never
run. Pull transfers ownership to you; the doc comment says so in
capitals.