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

  1. Check the wrapped response status — 4xx usually means the token already resolved or expired; treat as idempotent if a terminal result was already delivered.
  2. Send periodic `heartbeat(token)` calls while the external work runs to prevent timeout before reporting failure.
  3. Ensure only one terminal call (succeed or fail) is made per token — coordinate in the worker with a state flag.
  4. Verify `input.error` serializes via normalizeError (pass { errorType, errorMessage }-shaped data) and check IAM permissions.
  5. 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

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


AI-assisted analysis of anomalyco/sst@a0bd20f762 (2026-08-30). Data as JSON: /api/errors/28377d90b1449796. Report an issue: GitHub.