Skip to content

fix(stepfunctions): Choice.afterwards() excludes the otherwise branch from end states by default#37874

Open
Zelys-DFKH wants to merge 1 commit into
aws:mainfrom
Zelys-DFKH:fix/stepfunctions-choice-afterwards-include-otherwise
Open

fix(stepfunctions): Choice.afterwards() excludes the otherwise branch from end states by default#37874
Zelys-DFKH wants to merge 1 commit into
aws:mainfrom
Zelys-DFKH:fix/stepfunctions-choice-afterwards-include-otherwise

Conversation

@Zelys-DFKH
Copy link
Copy Markdown

Issue # (if applicable)

Closes #37649.

Credit to @taesungh for the clear diagnosis and reproduction steps, to @pahud for tracing the root cause to outgoingTransitions() unconditionally including defaultChoice regardless of includeOtherwise, and to @vishwakt for the design analysis on the feature-flag question.

Reason for this change

AfterwardsOptions.includeOtherwise is documented with @default false, meaning Choice.afterwards() should exclude the otherwise state from its returned end states by default. In practice the option was ignored. afterwards() called State.findReachableEndStates() without forwarding any flag about the default transition, and State.outgoingTransitions() always added defaultChoice to the traversal. The result: calling afterwards().next(Y) traversed into the otherwise branch, found states reachable from there, and called .next(Y) on them too. In the poll-loop pattern (where otherwise() points to a state already chained earlier), this threw StateAlreadyHasNextState instead of working correctly.

Description of changes

  • FindStateOptions gains readonly includeDefaultChoiceTransitions?: boolean. Default is true (i.e. undefined !== false), so bindToGraph, findReachableStates, and all external callers are unaffected. Only Choice.afterwards() passes this as false.
  • State.outgoingTransitions() gates the defaultChoice push on options.includeDefaultChoiceTransitions !== false.
  • Choice.afterwards() passes includeDefaultChoiceTransitions: options.includeOtherwise ?? false, excluding the otherwise branch by default and including it only when the caller explicitly opts in.

On feature flags: The old traversal contradicted the documented @default false for includeOtherwise. No code relying on that traversal had a documented guarantee to stand on. The AGENTS.md feature-flag criteria technically apply here (behavior change for previously-synthesizable code), so I'm happy to add a BugFix flag (aws-stepfunctions:respectAfterwardsIncludeOtherwise) if the team prefers.

Out of scope: The related case where choice.afterwards({ includeOtherwise: true }) throws when otherwise() was already called (noted by @taesungh in the issue thread) is a separate problem and not addressed here.

Describe any new or updated permissions being added

None.

Description of how you validated changes

New unit test 'afterwards() excludes the otherwise state from end states by default (poll-loop pattern)' in states-language.test.ts. It builds the exact poll-loop definition from the issue and asserts: (1) synth does not throw StateAlreadyHasNextState, and (2) the synthesized template shows Default: 'WaitToRepeat' still rendering correctly and DoOtherThing chained only through the Completed branch. The existing 'Can include OTHERWISE transition for Choice in afterwards()' test still passes, confirming includeOtherwise: true is unaffected. All 28 stepfunctions unit tests pass.

No integration test was added: this fix touches traversal logic only, with no new CloudFormation resource types, properties, or custom resources. An exemption comment will be added if the linter flags it.

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@Zelys-DFKH
Copy link
Copy Markdown
Author

Exemption request: this fix changes traversal logic only. No new CloudFormation resource types, properties, or custom resources are involved. The behavior change is fully covered by unit tests (28/28 pass), including a new regression test that directly reproduces the poll-loop failure from the issue.

@github-actions github-actions Bot added the beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK label May 14, 2026
@github-actions github-actions Bot added bug This issue is a bug. effort/medium Medium work item – several days of effort p2 labels May 14, 2026
@aws-cdk-automation aws-cdk-automation added the pr-linter/exemption-requested The contributor has requested an exemption to the PR Linter feedback. label May 14, 2026
Copy link
Copy Markdown
Collaborator

@aws-cdk-automation aws-cdk-automation left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pull request linter fails with the following errors:

❌ Fixes must contain a change to an integration test file and the resulting snapshot.

If you believe this pull request should receive an exemption, please comment and provide a justification. A comment requesting an exemption should contain the text Exemption Request. Additionally, if clarification is needed, add Clarification Request to a comment.

✅ A exemption request has been requested. Please wait for a maintainer's review.

@vishwakt
Copy link
Copy Markdown

Thanks for picking this up @Zelys-DFKH. The stepfunctions fix logic matches what was discussed on the issue and looks good to me.

One thing I noticed: the diff also includes unrelated changes to aws-ecs/lib/images/tag-parameter-container-image.ts (new imageDigest prop + tests). That seems independent of the Choice.afterwards() fix and the AGENTS.md guidance is explicit about this:

One concern per PR — submit cosmetic changes separately

Splitting the ECS change into its own PR would probably move both faster while this one stays a focused stepfunctions bugfix (which is what the exemption request is asking the linter to grant), and the ECS prop gets its own design review without being conflated with a traversal-logic fix.

Also, minor: the PR linter is asking for the title's first word not to be capitalized.

…m end states by default

AfterwardsOptions.includeOtherwise is documented with @default false but was
ignored. outgoingTransitions() always added defaultChoice to the traversal,
causing afterwards().next(Y) to traverse the otherwise branch and call next()
on states reachable from it. In poll-loop patterns this threw
StateAlreadyHasNextState.

Fix adds includeDefaultChoiceTransitions to FindStateOptions, gates the
defaultChoice push in outgoingTransitions() on it, and passes
options.includeOtherwise ?? false from Choice.afterwards(). All external
callers (bindToGraph, findReachableStates) are unaffected since the field
defaults to true (undefined !== false).

Fixes aws#37649.
@Zelys-DFKH Zelys-DFKH force-pushed the fix/stepfunctions-choice-afterwards-include-otherwise branch from df97c72 to b6c3050 Compare May 15, 2026 18:55
@Zelys-DFKH Zelys-DFKH changed the title fix(stepfunctions): Choice.afterwards() excludes the otherwise branch from end states by default fix(stepfunctions): Choice.afterwards() excludes the otherwise branch from end states by default May 15, 2026
@Zelys-DFKH
Copy link
Copy Markdown
Author

@vishwakt, appreciate the careful read. The ECS changes have their own PR (#37868). They crept onto this branch by mistake. Rebased and title fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK bug This issue is a bug. effort/medium Medium work item – several days of effort p2 pr-linter/exemption-requested The contributor has requested an exemption to the PR Linter feedback.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(stepfunctions): AfterwardsOptions.includeOtherwise not working as described/intended

3 participants