Current outcome
Let applications distinguish source failures, stage failures and individual item failures and correlate an item failure back to its input. The stream error channel remains diagnostics, not a durable completion or eligibility ledger.
Replanned on 2026-09-06 against GoBatch master 63ef757 and ShitQuant's recorder, enrichment, paper/replay, and flow workloads. This is an implementation target, not a claim that the behavior already exists.
Required behavior
Verification and completion
Test distinguishable source/stage/item representations, valid ID zero, wrapped causes and reporting cardinality on the existing emission path. Follow-up #87/#82 tests prove their new failure/skip boundaries. Reconcile send-site changes with #65.
Follow the repository's formatting, race-test, vet/lint, package documentation, example and changelog requirements for the changed surface. Report the actual supported behavior and migration; do not treat a passing coverage percentage as proof of these outcomes.
Scope and relationships
Does not force RequestBatcher to inherit stream semantics or block its independent result design. #87 owns stream continuation/filter rules; #91 owns pointer-wrapper usage docs and examples.
Design history
The earlier report/prototype remains below for provenance; the requirements above supersede conflicting prescriptions. Existing discussion is preserved.
Original issue: Error channel conflates per-item and processor-wide errors, and discards Item.ID
Summary
When doProcessors reports errors on the error channel, it loses two distinct pieces of information:
- 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.
- 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
Current outcome
Let applications distinguish source failures, stage failures and individual item failures and correlate an item failure back to its input. The stream error channel remains diagnostics, not a durable completion or eligibility ledger.
Replanned on 2026-09-06 against GoBatch master
63ef757and ShitQuant's recorder, enrichment, paper/replay, and flow workloads. This is an implementation target, not a claim that the behavior already exists.Required behavior
Verification and completion
Test distinguishable source/stage/item representations, valid ID zero, wrapped causes and reporting cardinality on the existing emission path. Follow-up #87/#82 tests prove their new failure/skip boundaries. Reconcile send-site changes with #65.
Follow the repository's formatting, race-test, vet/lint, package documentation, example and changelog requirements for the changed surface. Report the actual supported behavior and migration; do not treat a passing coverage percentage as proof of these outcomes.
Scope and relationships
Does not force RequestBatcher to inherit stream semantics or block its independent result design. #87 owns stream continuation/filter rules; #91 owns pointer-wrapper usage docs and examples.
Design history
The earlier report/prototype remains below for provenance; the requirements above supersede conflicting prescriptions. Existing discussion is preserved.
Original issue: Error channel conflates per-item and processor-wide errors, and discards Item.ID
Summary
When
doProcessorsreports errors on the error channel, it loses two distinct pieces of information:ProcessorError, so a caller usingerrors.As(err, &pe)cannot tell whether a whole processing stage failed or whether a single item failed within an otherwise-healthy batch.IDis thrown away. A per-item error is wrapped as&ProcessorError{Err: item.Error}with no reference to theItemit 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 theErrorfield") — the per-item tracking exists right up until the error hits the channel, where theIDis dropped and the error is relabeled as a generic stage error.Where
batch/batch.go, indoProcessors:Both sends produce the same
ProcessorErrortype, and the second discardsitem.ID.Impact
ID) failed.processor error: ....Itemcarrying both a uniqueIDand a per-itemErrorfield.Possible directions
(Design decision — listing options, not prescribing one.)
ItemErrortype that carries theID(and optionally the offendingItem/Data), e.g.:&ItemError{ItemID: item.ID, Err: item.Error}from the per-item path while keepingProcessorErrorfor stage-wide failures.IDtoProcessorError(optional field, zero when stage-wide) so existingerrors.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
Filtersilently dropping errored items, and fix(batch): panic recovery, deadlock-safe error sends, bounded prealloc, Done() race #65 documents theerrors.Aspointer-target form — but neither addresses the conflation or the lostIDdescribed here.ItemErroris additive (existingProcessorErrormatches break for per-item errors only); adding a field toProcessorErroris fully backwards-compatible. v0, so either is acceptable with a CHANGELOG note.References
batch/batch.go—doProcessorsper-item vs processor-wide error sends.batch/errors.go— currentProcessorError/SourceErrordefinitions.errors.Asguidance), Track consumer API hardening for bounded batch, request and flow workloads #75 (API hardening tracker).