Skip to content

Error channel conflates per-item and processor-wide errors, and discards Item.ID #79

Description

@MasterOfBinary

Summary

When doProcessors reports errors on the error channel, it loses two distinct pieces of information:

  1. Per-item errors and processor-wide errors are indistinguishable. Both are wrapped as ProcessorError, so a caller using errors.As(err, &pe) cannot tell whether a whole processing stage failed or whether a single item failed within an otherwise-healthy batch.
  2. The failing item's ID is thrown away. A per-item error is wrapped as &ProcessorError{Err: item.Error} with no reference to the Item it came from, so callers can't correlate the error back to the input item.

This contradicts the documented contract in CLAUDE.md ("Items with errors are tracked individually through the Error field") — the per-item tracking exists right up until the error hits the channel, where the ID is dropped and the error is relabeled as a generic stage error.

Where

batch/batch.go, in doProcessors:

var err error
items, err = proc.Process(ctx, items)
if err != nil {
    b.errs <- &ProcessorError{Err: err}        // batch.go:424 — processor-WIDE error
}
// ...
for _, item := range items {
    if item.Error != nil {
        b.errs <- &ProcessorError{Err: item.Error} // batch.go:430 — PER-ITEM error, ID lost
    }
}

Both sends produce the same ProcessorError type, and the second discards item.ID.

Impact

  • A caller cannot route/retry/dead-letter a specific failed item, because it has no idea which item (by ID) failed.
  • A caller cannot distinguish "stage N blew up" from "item 4,217 had a validation error" — both surface identically as processor error: ....
  • Defeats the purpose of Item carrying both a unique ID and a per-item Error field.

Possible directions

(Design decision — listing options, not prescribing one.)

  • Introduce a distinct ItemError type that carries the ID (and optionally the offending Item/Data), e.g.:
    type ItemError struct {
        ItemID uint64
        Err    error
    }
    func (e ItemError) Error() string { return fmt.Sprintf("item %d error: %v", e.ItemID, e.Err) }
    func (e ItemError) Unwrap() error { return e.Err }
    and send &ItemError{ItemID: item.ID, Err: item.Error} from the per-item path while keeping ProcessorError for stage-wide failures.
  • Alternatively, attach the ID to ProcessorError (optional field, zero when stage-wide) so existing errors.As(err, &ProcessorError{}) callers keep working.

Either way: stage-wide vs per-item should be distinguishable, and the per-item path should preserve Item.ID.

Notes

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions