anomalyco/sst · error · FailError
Failed to fail workflow callback
Error message
Failed to fail workflow callback
What it means
Thrown by the public `fail` callback function when the POST to `/2025-12-01/durable-execution-callbacks/{token}/fail` returns a non-OK HTTP status. It wraps the raw Response. The failure result was not delivered, so the workflow keeps waiting on the callback instead of resuming with the error.
Source
Thrown at sdk/js/src/aws/workflow.ts:465
token: string,
input: FailInput,
options?: Options,
): Promise<void> {
const response = await awsFetch(
"lambda",
`/2025-12-01/durable-execution-callbacks/${encodeURIComponent(
token,
)}/fail`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(normalizeError(input.error)),
},
options,
);
if (!response.ok) throw new FailError(response);
}
/**
* Send a heartbeat for a pending workflow callback.
*
* This is useful when the external system handling the callback is still doing
* work and needs to prevent the callback from timing out.
*
* This is the equivalent to calling
* [`SendDurableExecutionCallbackHeartbeat`](https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackHeartbeat.html).
*/
export async function heartbeat(
token: string,
options?: Options,
): Promise<void> {
const response = await awsFetch(
"lambda",
`/2025-12-01/durable-execution-callbacks/${encodeURIComponent(View on GitHub (pinned to a0bd20f762)
Solutions
- Check the wrapped response status — 4xx usually means the token already resolved or expired; treat as idempotent if a terminal result was already delivered.
- Send periodic `heartbeat(token)` calls while the external work runs to prevent timeout before reporting failure.
- Ensure only one terminal call (succeed or fail) is made per token — coordinate in the worker with a state flag.
- Verify `input.error` serializes via normalizeError (pass { errorType, errorMessage }-shaped data) and check IAM permissions.
- Retry 429/5xx with backoff.
Example fix
// before
await fail(token, { error: plainJsError }); // throws if token expired/used
// after
try {
await fail(token, { error: { errorType: "JobFailed", errorMessage: String(plainJsError) } });
} catch (err) {
if (err instanceof FailError && err.response.status >= 400 && err.response.status < 500) {
console.warn("callback already resolved or expired", token);
return;
}
throw err;
} Defensive patterns
Strategy: try-catch
Validate before calling
// ensure error is serializable and token used once
if (!token) throw new Error("missing callback token");
if (deliveredTokens.has(token)) return;
const error = { errorType: e.name ?? "Error", errorMessage: String(e.message ?? e) }; Type guard
function isFailError(err: unknown): err is FailError {
return err instanceof FailError && typeof err.response?.status === "number";
} Try / catch
try {
await fail(token, { error: { errorType: e.name, errorMessage: e.message } });
deliveredTokens.add(token);
} catch (err) {
if (err instanceof FailError && err.response.status >= 400 && err.response.status < 500) {
console.warn("callback expired or already resolved:", token, err.response.status);
return; // idempotent handling
}
throw err; // retry 429/5xx
} Prevention
- Decide once per token whether to succeed or fail — use a flag/state store so both paths can't fire.
- Send heartbeats while external work runs so the token doesn't time out before you report the failure.
- Pass errors as { errorType, errorMessage } so normalizeError serializes them reliably.
- Grant lambda:SendDurableExecutionCallbackFailure to the calling identity.
When it happens
Trigger: Calling `Workflow.fail(token, input)` where: (1) the token is expired, timed out, or already resolved (e.g. a success was already sent, or fail called twice); (2) the parent execution was stopped or deleted; (3) `input.error` cannot be normalized/serialized or the body is rejected; (4) credentials lack `lambda:SendDurableExecutionCallbackFailure`; (5) throttling/transient 5xx.
Common situations: External workers reporting failure after the callback window lapsed (no heartbeats sent); race between success and fail paths in worker code; requeue/redelivery causing a second fail for the same token; IAM policies missing the callback-failure action.
Related errors
- Failed to succeed workflow callback
- Failed to describe workflow
- Failed to stop workflow
- You must provide a KMS key via `kmsKey` when configuring `cu
- Cannot set both "logging.retention" and "logging.logGroup"
AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30).
Data as JSON: /api/errors/28377d90b1449796.
Report an issue: GitHub.