In AWS Step Functions, what JSON does a Catch block pass to the target state, and what changes if the catcher sets ResultPath to "$.error"?
answer
- the catcher produces a result too
- two fields, one of them a string
- default overwrites what came before
- choose where the error is placed
- null throws the error away
basics
~20 sA Step Functions catcher passes an object with two fields, Error and Cause. By default that object replaces the failed state's input entirely; setting ResultPath to "$.error" instead nests it inside the original input, so the recovery state still sees the business data it needs.
solid answer
~50 sWhen a catcher fires, Step Functions builds `{ "Error": "<error name>", "Cause": "<detail string>" }` and treats it as the state's result. A catcher's `ResultPath` defaults to `$`, meaning that object *becomes* the whole input of the `Next` state and the failed state's original input is gone. That is the classic bug: your compensating branch needs the order ID, and all it receives is an error blob. Setting `"ResultPath": "$.error"` merges instead — the target state receives the original input with the error object nested under an `error` key, so it can both identify the work and see why it failed. `"ResultPath": null` goes the other way, discarding the error and forwarding the untouched input. For Lambda-backed tasks, `Cause` is a string containing the serialised function error, so it usually needs parsing rather than direct field access.
code
json · 30 lines{
"StartAt": "ReserveInventory",
"States": {
"ReserveInventory": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "reserve", "Payload.$": "$" },
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "CompensateOrder"
}
],
"End": true
},
"CompensateOrder": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "compensate",
"Payload": {
"orderId.$": "$.orderId",
"reason.$": "$.error.Error"
}
},
"End": true
}
}
}go deeper
Remember that a caught failure arrives as an object with Error and Cause, and that Cause is a string. Know that ResultPath decides where that object lands in the next state's input.
Explain all three ResultPath cases — default $, a named path, and null — and why the default silently discards the failed state's input. Be able to show the merged JSON the target state receives.
Demonstrate that you design the recovery branch's contract deliberately: it needs identifiers to act on, a stable Error name to branch on, and a payload that will not blow the size quota once a stack trace is attached.
Set the standard for how failures are represented across many workflows — a consistent error envelope key, named domain errors rather than parsed strings, and a rule that recovery branches are exercised in testing rather than discovered in an incident.
## What the catcher actually produces When an error escapes retrying and a catcher in the `Catch` array matches it, Step Functions does not simply jump to another state — it *produces a result* for the failed state and then transitions. That result is always the same shape: ```json { "Error": "InventoryUnavailable", "Cause": "{\"errorMessage\":\"sku 88213 out of stock\", ...}" } ``` `Error` is the error name that matched. `Cause` is a **string**, not an object. For a Lambda-backed task it holds the serialised function error — message, type, and usually a stack trace — which means a downstream state that wants a field out of it has to parse the string, not path into it. Designing your workflow to branch on `Error` rather than to inspect `Cause` is the more robust choice, and it is why raising typed, named errors is worth the effort. ## ResultPath decides what the next state sees `ResultPath` is the ordinary Amazon States Language field that says *where a state's result is placed relative to that state's input*. It behaves the same way on a catcher as anywhere else, and the three cases are: - **`"$"` (the default)** — the result replaces the input. The `Next` state receives only `{ "Error": ..., "Cause": ... }`. - **A path such as `"$.error"`** — the result is inserted into a copy of the input at that key. The `Next` state receives the original input *plus* an `error` field. - **`null`** — the result is discarded and the input passes through untouched. The `Next` state sees the original input and nothing about the failure. The default is the trap. It is easy to write a catcher that routes to `CompensateOrder`, and only discover in production that `CompensateOrder` has no idea which order to compensate, because the input it needed was overwritten by an error blob. The symptom is a recovery branch that fails on a missing field — a failure inside your failure handling, which is a bad place to be. ## What good looks like ```json "Catch": [ { "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "CompensateOrder" } ] ``` With the state's input being `{"orderId": "A-77", "items": 3}`, `CompensateOrder` now receives: ```json { "orderId": "A-77", "items": 3, "error": { "Error": "InventoryUnavailable", "Cause": "..." } } ``` It can release the hold on order `A-77` *and* record the reason. If instead you genuinely want the recovery branch to behave as though the failure carried no extra information — for example, it just retries a different path with the same payload — `"ResultPath": null` is the honest expression of that. ## Two related details worth knowing First, a path that cannot be applied to the input is a runtime failure, not a silent no-op: Step Functions raises `States.ResultPathMatchFailure`. Since this happens *inside* your error handling, it is worth keeping the path simple — a single top-level key such as `$.error` — rather than something that assumes a nested structure exists. Second, nesting the error grows the payload rather than replacing it. Every state's input and output is bounded by a payload quota, and a large `Cause` — a long stack trace, for example — accumulating alongside a big business payload can push a state over that boundary and raise `States.DataLimitExceeded`. On workflows carrying substantial data, catching to a state that immediately trims the payload down to identifiers is the defensive move. ## Why this question separates people Anyone who has drawn a state machine on a whiteboard knows a catcher routes to another state. Only someone who has *shipped* one knows the target state's input silently changes shape when it does. It is a small piece of ASL semantics with an outsized effect on whether the failure path actually works when it is finally exercised — which, by definition, is the worst possible moment to find out that it does not.
- Why is branching on Error more robust than reading fields out of Cause?`Cause` is a free-form string — for Lambda tasks it holds the serialised function error including a stack trace, and its content changes with the runtime, the library, and the error itself. `Error` is a stable, deliberately chosen name, either your exception type or one you set on a `Fail` state. Routing on a stable name survives refactoring; parsing a stack trace does not.
- What happens if the ResultPath on a catcher cannot be applied to the state's input?Step Functions raises `States.ResultPathMatchFailure`. That is a failure occurring inside your failure handling, which is why a simple top-level path such as `$.error` is safer than one that assumes some nested object already exists in the payload.
- When would you deliberately set ResultPath to null on a catcher?When the recovery branch needs the original payload and genuinely has no use for the failure detail — for example it re-routes the same input down an alternative path, or hands it to a queue for human review. It also keeps the payload from growing, which matters when the business data is already large and `Cause` carries a long stack trace.
saying these in an interview costs you the question
- Assuming the target state still receives the original input by default
- Treating Cause as a JSON object you can path into
- Branching on substrings of Cause instead of the Error name
- Thinking ResultPath only applies to successful results, not catchers
- Ignoring that a large Cause grows the payload toward the size quota