Guide · AWS Step Functions
MCP Server Step Functions Error Handling — Retry, Catch, ResultSelector, and intrinsic functions
Step Functions expresses all retry and error-handling logic declaratively in the ASL state machine definition — the code that runs the tool does not need to implement exponential backoff, catch clauses, or error routing. The two ASL constructs are Retry (retry the same task on specified error names, with configurable backoff, max attempts, max delay, and jitter) and Catch (route to a different state on specified error names, merging the error details into the execution state via ResultPath). The four data-flow operators that determine what the next state receives are ResultSelector (reshape the raw task output before it enters the state), ResultPath (where in the state the task result is merged), OutputPath (which slice of the post-merge state is passed forward), and intrinsic functions (States.Format, States.JsonMerge, States.ArrayLength, States.StringToJson, States.JsonToString) that allow inline JSON manipulation without a Lambda pass-through state.
TL;DR
Put States.ALL last in every Retry and Catch array — it is a catch-all that must come after specific error names or it will shadow them. Use JitterStrategy: "FULL" on Retry to prevent thundering-herd retries when multiple executions fail simultaneously. Set MaxDelaySeconds to cap exponential backoff at a sensible ceiling (e.g., 60 seconds) — without it, BackoffRate 2 with MaxAttempts 6 produces a maximum wait of 2⁵ × IntervalSeconds = 32× IntervalSeconds. Use ResultPath: "$.error" on Catch to preserve the execution's input alongside the error details; use ResultPath: null to discard the task result entirely. Never use OutputPath: "$" on an error handler — it passes the raw error object forward instead of the accumulated execution state.
Retry: exponential backoff with jitter and MaxDelaySeconds
The Retry array on a Task state specifies retry behavior when the task throws an error. Each entry in the array has ErrorEquals (array of error names), IntervalSeconds (initial wait before first retry), MaxAttempts (total retries, default 3), BackoffRate (multiplier applied to IntervalSeconds between retries), MaxDelaySeconds (ceiling on the computed backoff interval), and JitterStrategy ("FULL" randomizes each retry interval between 0 and the computed value).
Error names for Step Functions built-in errors start with States.: States.TaskFailed, States.Timeout, States.HeartbeatTimeout, States.Permissions, States.ResultPathMatchFailure, States.ParameterPathFailure, States.BranchFailed, States.NoChoiceMatched, States.IntrinsicFailure. Lambda functions throw errors with the error's class name (e.g., Error, ValidationError, RateLimitExceeded) — the error code in ErrorEquals must match the Lambda function's thrown error name exactly.
// ASL Task state with layered Retry strategy
const taskWithRetry = {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:call-external-api",
"Retry": [
{
// Retry rate limit errors more aggressively with longer backoff
"ErrorEquals": ["RateLimitExceeded", "TooManyRequestsError"],
"IntervalSeconds": 5,
"MaxAttempts": 5,
"BackoffRate": 2.0,
"MaxDelaySeconds": 60, // cap at 60 seconds regardless of BackoffRate
"JitterStrategy": "FULL" // randomize to prevent retry storms
},
{
// Retry transient infrastructure failures quickly
"ErrorEquals": ["States.TaskFailed", "ServiceUnavailableError"],
"IntervalSeconds": 1,
"MaxAttempts": 3,
"BackoffRate": 2.0,
"MaxDelaySeconds": 10,
"JitterStrategy": "FULL"
},
{
// Catch-all retry — always put States.ALL last
"ErrorEquals": ["States.ALL"],
"IntervalSeconds": 2,
"MaxAttempts": 2,
"BackoffRate": 1.5
}
],
"Catch": [
{
// Handle permanent failures after all retries exhausted
"ErrorEquals": ["ValidationError", "InvalidInputError"],
"Next": "HandleValidationError",
"ResultPath": "$.validationError"
},
{
"ErrorEquals": ["States.ALL"],
"Next": "HandleUnexpectedError",
"ResultPath": "$.unexpectedError"
}
],
"Next": "ProcessApiResult"
};
The Retry interval calculation: wait = min(IntervalSeconds × BackoffRate^(attemptNumber - 1), MaxDelaySeconds). With IntervalSeconds: 1, BackoffRate: 2, MaxAttempts: 6, and no MaxDelaySeconds, the waits are: 1s, 2s, 4s, 8s, 16s, 32s. Adding MaxDelaySeconds: 10 caps them at: 1s, 2s, 4s, 8s, 10s, 10s. With JitterStrategy: "FULL", each interval is further randomized uniformly between 0 and the computed value.
ResultSelector and ResultPath: shaping task output into execution state
ResultSelector reshapes the raw task output using JSONPath and intrinsic functions before it is merged into the execution state. This avoids adding a separate Pass or Lambda state just to extract a nested field or rename a key. ResultPath controls where the (possibly reshaped) result is placed in the execution state — "$.result" merges the result under that key; null discards the result entirely; "$" (the default) replaces the entire state with the result.
// Task state using ResultSelector to extract only needed fields from Lambda output
const taskWithResultSelector = {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:fetch-data",
// Raw Lambda output: { statusCode: 200, headers: {...}, body: '{"items":[...],"count":42}' }
"ResultSelector": {
// Extract just the items and count; parse the nested JSON string
"items.$": "States.StringToJson($.body).items",
"count.$": "States.StringToJson($.body).count",
"fetchedAt.$": "$$.Execution.StartTime" // inject execution context
},
// Merge the selected result into execution state at $.fetchResult
// Execution state retains its previous structure; only $.fetchResult is added/replaced
"ResultPath": "$.fetchResult",
// After ResultPath merge, pass only the fields the next state needs
"OutputPath": "$.fetchResult",
"Next": "ProcessItems"
};
// Task state on Catch that PRESERVES input state alongside error
const taskWithPreservingCatch = {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:risky-operation",
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "HandleError",
// ResultPath: "$.error" merges error into state — execution input is preserved
// The error handler receives { ...originalInput, error: { Error: "...", Cause: "..." } }
"ResultPath": "$.error"
}
],
"Next": "Success"
};
// WRONG: ResultPath: "$" on a Catch replaces the entire state with the error object
// The error handler receives only { Error: "...", Cause: "..." } — original input is LOST
const badCatch = {
"ErrorEquals": ["States.ALL"],
"Next": "HandleError",
"ResultPath": "$" // do NOT use — discards all accumulated execution state
};
Intrinsic functions: inline data manipulation without Lambda
Step Functions intrinsic functions allow JSON manipulation directly in ASL without a Lambda pass-through state. They are called in Parameters, ResultSelector, and ItemSelector fields using the .$ suffix convention. The key functions for MCP tool orchestration:
States.Format — string interpolation with {} placeholders. Arguments after the template string are substituted in order. Useful for building S3 keys, API paths, or log messages from execution context.
States.JsonMerge — shallow merge of two JSON objects. Takes two objects and a boolean (whether to deep-merge). Deep merge is not recursive — it only affects the second level.
States.StringToJson / States.JsonToString — parse a JSON string to an object or serialize an object to a JSON string. Required when Lambda returns a stringified JSON body (common in HTTP responses).
States.ArrayLength, States.ArrayGetItem, States.ArrayUnique — array operations without Lambda. ArrayLength counts items; ArrayGetItem accesses by index; ArrayUnique deduplicates.
States.MathAdd, States.MathRandom — arithmetic. MathRandom generates a random float between two bounds (requires a seed from $$.Execution.Id for reproducibility).
// State using intrinsic functions in Parameters
const stateWithIntrinsics = {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123:function:process-document",
"Parameters": {
// Build an S3 key from execution context and input fields
"s3Key.$": "States.Format('executions/{}/{}/output.json', $$.Execution.Name, $.documentId)",
// Merge base config with per-execution overrides
"config.$": "States.JsonMerge($.baseConfig, $.overrides, false)",
// Parse a JSON string from a previous Lambda response
"parsedBody.$": "States.StringToJson($.httpResponse.body)",
// Count how many items to process
"itemCount.$": "States.ArrayLength($.items)",
// Build a log message inline
"logMessage.$": "States.Format('Processing {} items for execution {}', States.ArrayLength($.items), $$.Execution.Name)"
},
"Next": "AggregateResults"
};
// Pass state using JsonMerge to combine two objects from different execution paths
const mergeResultsState = {
"Type": "Pass",
"Parameters": {
// Merge primary result with metadata — shallow merge (third arg = false)
"combined.$": "States.JsonMerge($.primaryResult, $.metadata, false)",
// Serialize the merged object to a JSON string for a downstream system that expects a string
"combinedJson.$": "States.JsonToString(States.JsonMerge($.primaryResult, $.metadata, false))"
},
"ResultPath": "$.merged",
"Next": "WriteToS3"
};
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| States.ALL before specific error names in Retry/Catch array | Specific handlers never reached; all errors match the catch-all | Always put specific error names first; States.ALL must be the last entry in the array |
| No MaxDelaySeconds on BackoffRate > 1 | Retry intervals grow unbounded: BackoffRate 2 with MaxAttempts 8 waits up to 128× IntervalSeconds | Set MaxDelaySeconds to a sensible ceiling (60s for API calls, 10s for transient infrastructure) |
| ResultPath: "$" on Catch handler | Error handler receives only { Error, Cause } — all accumulated execution input is lost | Use ResultPath: "$.error" to merge error alongside input state; use ResultPath: null to discard error details |
| Lambda error class name doesn't match ErrorEquals | Specific Catch/Retry for that error never triggers; falls through to States.ALL | Lambda error code = thrown error's class name (e.g., "ValidationError" not "Error: ValidationError"); test with execution history |
| States.StringToJson on non-JSON string | States.IntrinsicFailure; execution fails at parameter binding | Validate that Lambda body is always valid JSON before using StringToJson; add a guard Lambda for untrusted inputs |
| OutputPath: "$" after Catch | Next state receives the raw error object { Error, Cause } instead of merged execution state | Use OutputPath: "$" only when the entire post-merge state should be passed; after Catch, usually omit OutputPath or use "$.someField" |