anomalyco/sst · error · StopError

Failed to stop workflow

Error message

Failed to stop workflow

What it means

Thrown by the public `stop` function when the POST to `/2025-12-01/durable-executions/{arn}/stop` returns a non-OK HTTP status. It wraps the raw Response so the caller can inspect status and body. This means the execution was not stopped — it may still be running and consuming retries/time.

Source

Thrown at sdk/js/src/aws/workflow.ts:396

  ): Promise<StopResponse> {
    const response = await awsFetch(
      "lambda",
      `/2025-12-01/durable-executions/${encodeURIComponent(arn)}/stop`,
      {
        method: "POST",
        headers: input?.error
          ? {
              "Content-Type": "application/json",
            }
          : undefined,
        body:
          input?.error === undefined
            ? undefined
            : JSON.stringify(normalizeError(input.error)),
      },
      options,
    );
    if (!response.ok) throw new StopError(response);

    const data = (await response.json()) as StopInvocationResponse;
    return {
      arn,
      status: "STOPPED",
      stoppedAt:
        data.StopTimestamp === undefined
          ? undefined
          : parseTimestamp(data.StopTimestamp),
    };
  }

  /**
   * Send a successful result for a pending workflow callback.
   *
   * This is the equivalent to calling
   * [`SendDurableExecutionCallbackSuccess`](https://docs.aws.amazon.com/lambda/latest/api/API_SendDurableExecutionCallbackSuccess.html).
   */

View on GitHub (pinned to a0bd20f762)

Solutions

  1. Check the wrapped response status — 404/409 usually means the execution already terminated, which may be acceptable; treat idempotently.
  2. Verify IAM permissions for stopping durable executions in the target region/account.
  3. Retry 429/5xx with backoff before giving up.
  4. Validate the optional `input.error` object matches the expected shape ({ errorType, errorMessage }) when passing it.

Example fix

// before
await stop(arn); // throws if already stopped
// after
try {
  await stop(arn, { error: { errorType: "Cancelled", errorMessage: "user cancelled" } });
} catch (err) {
  if (err instanceof StopError && [404, 409].includes(err.response.status)) return; // already terminal
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// check current state first; skip stop if already terminal
const current = await describe(arn).catch(() => null);
if (!current || ["SUCCEEDED", "FAILED", "STOPPED"].includes(current.status)) {
  return; // nothing to stop
}

Type guard

function isStopError(err: unknown): err is StopError {
  return err instanceof StopError && typeof err.response?.status === "number";
}

Try / catch

try {
  await stop(arn, { error: { errorType: "Cancelled", errorMessage: "user requested stop" } });
} catch (err) {
  if (err instanceof StopError && [404, 409].includes(err.response.status)) {
    return; // already terminal — treat stop as idempotent
  }
  if (err instanceof StopError && (err.response.status === 429 || err.response.status >= 500)) {
    return retryWithBackoff(() => stop(arn));
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling `Workflow.stop(arn)` where: (1) the ARN does not exist or was already completed (404/409 — execution already SUCCEEDED/FAILED/STOPPED); (2) credentials lack the stop permission (`lambda:StopDurableExecution`); (3) throttling (429) or transient 5xx; (4) malformed error payload in `input.error` rejected by the API.

Common situations: Race condition where the workflow finished before the stop call landed; cancelling stale executions from a cleanup script against the wrong region; IAM policy missing the durable-execution stop action; double-stopping the same execution from concurrent workers.

Related errors


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