Repository navigation
Description
Expected Behavior
BatchResult.succeeded() returns every item whose status is SUCCEEDED, so succeeded().length equals successCount.
BatchResult.getResults() returns one entry per succeeded item, in branch order, carrying undefined for an item whose function returned nothing.
The same holds on the error side. failed() returns every item whose status is FAILED, and getErrors() returns one entry per failed item.
Actual Behavior
Both filter on the item's value as well as its status, so a succeeded item whose function returned undefined is dropped from both.
Example, ctx.map over four items where two return undefined (observed on the live BatchResult inside the handler):
all_statuses: ["SUCCEEDED", "SUCCEEDED", "SUCCEEDED", "SUCCEEDED"]
all_result_types: ["undefined", "number", "undefined", "number"]
successCount: 4
succeeded_len: 2 <- expected 4
getResults: [10, 30] <- expected [undefined, 10, undefined, 30]
So succeeded() disagrees with successCount on the same batch, and getResults() is no longer index-aligned with the items that produced it. A caller cannot map an output back to its input.
This affects both ctx.map and ctx.parallel.
Note: failed() and getErrors() carry the same value filter, so a failed item with no error would be dropped the same way. But this currently can't be reproduced, see additional context section.
Steps to Reproduce
- Write a durable function that calls ctx.map or ctx.parallel over several branches.
- Make every branch succeed, at least one branch function should return undefined, and at least one should return a value.
- From the returned BatchResult, compare successCount against succeeded().length, and compare BatchResult.all against getResults().
Every branch appears in BatchResult.all with status SUCCEEDED, and successCount counts every one of them. But succeeded() and getResults() each omit the branches that returned undefined, so both are shorter than the number of succeeded items and getResults() is no longer index-aligned with BatchResult.all.
The error-side filters in failed() and getErrors() have no reproduction, see additional context section.
SDK Version
2.6.0
Node.js Version
22.x
Is this a regression?
No
Last Working Version
No response
Additional Context
Root cause. Two BatchResult accessors add a value check to the status check:
// batch-result.ts#L128, succeeded(): item.status === BatchItemStatus.SUCCEEDED && item.result !== undefined
// batch-result.ts#L138, failed(): item.status === BatchItemStatus.FAILED && item.error !== undefined
getResults() calls succeeded().map(item => item.result), and getErrors() calls failed().map(item => item.error), so both inherit the same filter from the method they call.
undefined is a legitimate result. A branch function that returns nothing produces a BatchItem with status SUCCEEDED and result undefined, which the filter cannot distinguish from "no result", so the item is dropped.
The error side has the identical defect, but it cannot be reproduced today. No path builds a FAILED BatchItem with error undefined: live construction always synthesizes an error in its catch block, and replay always throws out of runInChildContext on a FAILED status (checkpointed error, or a generic ChildContextError when absent) before the item is built.
The bug is in the shared succeeded()/failed() filters, not in either construction path, so it reproduces the same way whether the BatchResult comes from live execution or from checkpoint replay. Forcing a suspend/resume mid-map (one item waits, so the already-finished items are rebuilt from their child checkpoints via replayItems on the next invocation) gives the same numbers as the live run: succeeded_len 2, getResults [10, 30].
Activity