Skip to content

Error handling

Retry behavior

Errors inside a step

When your code throws an exception from inside a step, the SDK retries the step according to its retry strategy. The SDK checkpoints each retry attempt, ends the Lambda invocation and then the backend re-invokes the Lambda function at the required time. This means you do not consume compute time while waiting between retry attempts.

Errors outside a step

When your code throws an exception from outside a step, the SDK marks the execution as FAILED and returns the error immediately. The SDK does not retry it. Wrap error-prone code in a step for automatic retry.

When retries exhaust

When a step exhausts all retry attempts, the SDK checkpoints the final error and throws it to your handler. You can catch it there and handle it as required.

import { withDurableExecution, StepError } from "@aws/durable-execution-sdk-js";

export const handler = withDurableExecution(async (event, context) => {
  try {
    const result = await context.step("process-order", async (stepCtx) => {
      const orderId = (event as { orderId?: string }).orderId;
      if (!orderId) {
        throw new Error("orderId is required");
      }
      return { orderId, status: "processed" };
    });
    return result;
  } catch (err) {
    if (err instanceof StepError) {
      context.logger.error("Step failed", { cause: err.cause?.message });
      return { error: err.cause?.message ?? "Step failed" };
    }
    throw err;
  }
});
from aws_durable_execution_sdk_python import StepError
from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
from aws_durable_execution_sdk_python.execution import durable_execution


@durable_step
def process_order(step_context: StepContext, order_id: str) -> dict:
    if not order_id:
        raise ValueError("order_id is required")
    return {"order_id": order_id, "status": "processed"}


@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
    try:
        result = context.step(process_order(event.get("orderId", "")))
        return result
    except StepError as e:
        context.logger.error("Step failed", extra={"error_type": e.error_type})
        return {"error": e.message}
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.exception.StepFailedException;

public class BasicErrorHandling extends DurableHandler<Map<String, String>, Map<String, Object>> {

    @Override
    public Map<String, Object> handleRequest(Map<String, String> event, DurableContext context) {
        try {
            Map<String, String> result = context.step("process-order", Map.class, stepCtx -> {
                String orderId = event.get("orderId");
                if (orderId == null || orderId.isEmpty()) {
                    throw new IllegalArgumentException("orderId is required");
                }
                return Map.of("orderId", orderId, "status", "processed");
            });
            return Map.of("result", result);
        } catch (StepFailedException e) {
            context.getLogger().error("Step failed: " + e.getMessage());
            return Map.of("error", e.getErrorObject().errorMessage());
        }
    }
}

The step body returns an error, and Step returns a *durable.StepError when no retry remains. Match it with errors.As(err, &stepErr) and read stepErr.Message. The wrapped Err is a stand-in rebuilt from the record, so matching the step body's own error type never succeeds. Return any other error unchanged. It can be the signal that the invocation is suspending, which matches no public type.

package main

import (
    "errors"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type orderEvent struct {
    OrderID string `json:"orderId"`
}

func handler(ctx durable.Context, event orderEvent) (map[string]string, error) {
    result, err := durable.Step(ctx, "process-order", func(_ durable.StepContext) (map[string]string, error) {
        if event.OrderID == "" {
            return nil, errors.New("orderId is required")
        }
        return map[string]string{"orderId": event.OrderID, "status": "processed"}, nil
    })
    if err != nil {
        var stepErr *durable.StepError
        if errors.As(err, &stepErr) {
            ctx.Logger().Error("Step failed", "cause", stepErr.Message)
            return map[string]string{"error": stepErr.Message}, nil
        }
        return nil, err
    }
    return result, nil
}

func main() { durable.Start(handler) }
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;

public class BasicErrorHandlingExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<OrderEvent, OrderResult>(Workflow, input, context);

    private async Task<OrderResult> Workflow(OrderEvent input, IDurableContext ctx)
    {
        try
        {
            OrderResult result = await ctx.StepAsync(
                async (_, _) =>
                {
                    if (string.IsNullOrEmpty(input.OrderId))
                    {
                        throw new InvalidOperationException("orderId is required");
                    }
                    return new OrderResult(input.OrderId, Status: "processed");
                },
                name: "process-order");
            return result;
        }
        catch (StepException e)
        {
            // The step exhausted its retries; the SDK checkpointed the final
            // error and threw it here.
            ctx.Logger.LogError("Step failed: {Message}", e.Message);
            return new OrderResult(input.OrderId, Status: "error", Error: e.Message);
        }
    }
}

public record OrderEvent(string OrderId);
public record OrderResult(string OrderId, string Status, string? Error = null);

Replay throws the same error

When the SDK replays a completed operation from its checkpoint, it returns the checkpointed result without re-running the step body. If the checkpointed result was an error, the SDK re-throws that same error at the same point in your code. Your handler sees the same exception it did on the original execution.

Exception types

Each SDK wraps step failures in its own exception type. The type carries the original error and the operation details.

graph TD
  DOE[DurableOperationError]
  DOE --> StepError
  DOE --> CallbackError
  CallbackError --> CallbackExternalError
  CallbackError --> CallbackTimeoutError
  CallbackError --> CallbackSubmitterError
  DOE --> InvokeError
  DOE --> ChildContextError
  DOE --> PromiseCombinatorError
  DOE --> WaitForConditionError
  SIE["StepInterruptedError (retryStrategy callback only)"]
import {
  DurableOperationError,
  StepError,
  CallbackError,
  CallbackExternalError,
  CallbackTimeoutError,
  CallbackSubmitterError,
  InvokeError,
  ChildContextError,
  PromiseCombinatorError,
  WaitForConditionError,
  StepInterruptedError,
} from "@aws/durable-execution-sdk-js";

// DurableOperationError
//   StepError:               step failed after retries exhausted
//   CallbackError:           parent class for callback failures
//     CallbackExternalError:   external entity reported failure
//     CallbackTimeoutError:    callback timed out
//     CallbackSubmitterError:  submitter function failed
//   InvokeError:             invoke operation failed
//   ChildContextError:       child context failed
//   PromiseCombinatorError:  context.promise.{all,allSettled,any,race} failed
//   WaitForConditionError:   wait-for-condition failed
//
// StepInterruptedError: internal sentinel. The SDK passes it to your
//   retryStrategy(error, attempt) callback when Lambda interrupts an
//   at-most-once step. From context.step(...) the SDK throws a StepError
//   whose cause.name === "StepInterruptedError".

export {
  DurableOperationError,
  StepError,
  CallbackError,
  CallbackExternalError,
  CallbackTimeoutError,
  CallbackSubmitterError,
  InvokeError,
  ChildContextError,
  PromiseCombinatorError,
  WaitForConditionError,
  StepInterruptedError,
};

DurableOperationError is the base class for all operation-level failures. Each subclass corresponds to a specific operation type. The cause property holds the original error your code threw.

CallbackError is the parent class for callback failures. CallbackExternalError reports a failure the external entity sent through SendDurableExecutionCallbackFailure. CallbackTimeoutError signals a callback that exceeded its timeout. CallbackSubmitterError wraps a failure raised inside the submitter function. The SDK throws CallbackError directly only for internal callback failures that do not match a more specific subclass. Catch CallbackError to handle every callback failure with a single branch.

PromiseCombinatorError wraps failures from the durable promise combinators context.promise.all, context.promise.allSettled, context.promise.any, and context.promise.race.

The TypeScript SDK passes StepInterruptedError to your retryStrategy(error, attempt) callback when Lambda interrupts an at-most-once step before the SDK checkpoints the result. From context.step(...) the SDK throws a StepError whose cause.name equals "StepInterruptedError". See Step interrupted below.

graph TD
  DEE[DurableExecutionsError]
  DEE --> ValidationError
  DEE --> SerDesError
  DEE --> UnrecoverableError
  DEE --> DOE[DurableOperationError]
  UnrecoverableError --> ExecutionError
  ExecutionError --> NonDeterministicExecutionError
  UnrecoverableError --> InvocationError
  InvocationError --> StepInterruptedError
  InvocationError --> RetryableSerDesError
  DOE --> StepError
  DOE --> InvokeError
  DOE --> ChildContextError
  DOE --> WaitForConditionError
  DOE --> BatchCompletionError
  DOE --> CallbackError
  CallbackError --> CallbackExternalError
  CallbackError --> CallbackTimeoutError
  CallbackError --> CallbackSubmitterError
from aws_durable_execution_sdk_python import (
    BatchCompletionError,
    CallbackError,
    CallbackExternalError,
    CallbackSubmitterError,
    CallbackTimeoutError,
    ChildContextError,
    DurableExecutionsError,
    DurableOperationError,
    ExecutionError,
    InvocationError,
    InvokeError,
    RetryableSerDesError,
    SerDesError,
    StepError,
    ValidationError,
    WaitForConditionError,
)

# DurableExecutionsError                 — base for all SDK exceptions
#   ValidationError                      — invalid arguments to SDK operations
#   SerDesError                          — permanent serialize/deserialize failure
#   UnrecoverableError
#     ExecutionError                     — fails execution without retry
#       NonDeterministicExecutionError   — replay diverged from recorded history
#     InvocationError                    — triggers Lambda retry
#       StepInterruptedError             — at-most-once step interrupted
#       RetryableSerDesError             — transient serdes failure, retries
#   DurableOperationError                — base for per-operation failures
#     StepError                          — step exhausted its retries
#     InvokeError                        — chained invoke failed
#     ChildContextError                  — child context failed
#     WaitForConditionError              — wait_for_condition exhausted attempts
#     BatchCompletionError               — custom completion predicate failed the batch
#     CallbackError                      — base for callback failures
#       CallbackExternalError            — external system reported failure
#       CallbackTimeoutError             — callback or heartbeat timed out
#       CallbackSubmitterError           — submitter function raised

DurableExecutionsError is the base class for all SDK exceptions.

DurableOperationError is the base class for per-operation failures. StepError, InvokeError, ChildContextError, and WaitForConditionError each report a failure from the matching operation. BatchCompletionError reports a map or parallel batch that a custom completion predicate marked failed. Inspect the failure through its error_type, message, data, and stack_trace attributes. The SDK reconstructs a DurableOperationError stand-in carrying those fields on both the first run and replay, so the SDK does not preserve the original exception type.

CallbackError is the parent class for callback failures. CallbackExternalError reports a failure the external entity sent through SendDurableExecutionCallbackFailure. CallbackTimeoutError signals a callback that exceeded its timeout. CallbackSubmitterError wraps a failure raised inside the submitter function. Catch CallbackError to handle every callback failure with a single branch.

SerDesError reports a permanent serialization or deserialization failure and does not retry. RetryableSerDesError signals a transient one that fails the invocation so the backend retries it.

ExecutionError fails the execution without retry. InvocationError causes Lambda to retry the entire invocation. Both carry a termination_reason attribute. NonDeterministicExecutionError is a subclass of ExecutionError that the SDK raises when replay diverges from the recorded execution history.

StepInterruptedError is a subclass of InvocationError. The SDK raises it when an at-most-once step started but Lambda was interrupted before the SDK checkpointed the result. See Step interrupted below.

graph TD
  DEE[DurableExecutionException]
  DEE --> UDEE[UnrecoverableDurableExecutionException]
  DEE --> DOE[DurableOperationException]
  DEE --> SerDesException
  UDEE --> IllegalDurableOperationException
  UDEE --> NonDeterministicExecutionException
  DOE --> StepException
  DOE --> CallbackException
  StepException --> StepFailedException
  StepException --> StepInterruptedException
  CallbackException --> CallbackFailedException
import software.amazon.lambda.durable.exception.DurableExecutionException;
import software.amazon.lambda.durable.exception.DurableOperationException;
import software.amazon.lambda.durable.exception.UnrecoverableDurableExecutionException;
import software.amazon.lambda.durable.exception.StepException;
import software.amazon.lambda.durable.exception.StepFailedException;
import software.amazon.lambda.durable.exception.StepInterruptedException;
import software.amazon.lambda.durable.exception.CallbackException;
import software.amazon.lambda.durable.exception.CallbackFailedException;
import software.amazon.lambda.durable.exception.SerDesException;
import software.amazon.lambda.durable.exception.IllegalDurableOperationException;
import software.amazon.lambda.durable.exception.NonDeterministicExecutionException;

// DurableExecutionException
//   UnrecoverableDurableExecutionException — execution terminated immediately
//     IllegalDurableOperationException     — illegal SDK operation detected
//     NonDeterministicExecutionException   — non-deterministic replay detected
//   DurableOperationException              — operation-level failure with details
//     StepException
//       StepFailedException                — step failed after retries exhausted
//       StepInterruptedException           — at-most-once step interrupted
//     CallbackException
//       CallbackFailedException            — callback failed with error from external system
//   SerDesException                        — serialization or deserialization failed

DurableExecutionException is the base class for all SDK exceptions.

DurableOperationException carries the failed Operation and its ErrorObject. Use getOperation(), getErrorObject(), and getOperationStatus() to inspect the failure.

StepFailedException is thrown when a step exhausts all retry attempts.

StepInterruptedException is thrown when an at-most-once step started but Lambda was interrupted before the SDK checkpointed the result. See Step interrupted below.

graph TD
  OE["OperationError"]
  SE[StepError]
  SIE[StepInterruptedError]
  IE[InvokeError]
  CE[CallbackError]
  CCE[ChildContextError]
  WCE[WaitForConditionError]
  RE[RetryError]
  CbE[CombinatorError]
  BE[BatchError]
  BCE[BatchCompletionError]
  NDE[NonDeterministicExecutionError]
  SE -. errors.As .-> OE
  SIE -. errors.As .-> OE
  IE -. errors.As .-> OE
  CE -. errors.As .-> OE
  CCE -. errors.As .-> OE
  WCE -. errors.As .-> OE
  RE -. errors.As .-> OE
  CbE -. errors.As .-> OE
  BE -. errors.As .-> OE
  BCE -. errors.As .-> OE
  NDE -. errors.As .-> OE
  CEE[CallbackExternalError] -->|embeds| CE
  CTE[CallbackTimeoutError] -->|embeds| CE
  CSE[CallbackSubmitterError] -->|embeds| CE
package main

import (
    "errors"
    "fmt"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type report struct {
    MatchedStepError      bool   `json:"matchedStepError"`
    MatchedOperationError bool   `json:"matchedOperationError"`
    Name                  string `json:"name"`
    ErrorType             string `json:"errorType"`
}

func handler(ctx durable.Context, _ any) (report, error) {
    _, err := durable.Step(ctx, "failing-step", func(_ durable.StepContext) (any, error) {
        return nil, fmt.Errorf("deliberate failure")
    }, durable.WithRetry(durable.NoRetry()))

    var r report
    var stepErr *durable.StepError
    if errors.As(err, &stepErr) {
        r.MatchedStepError = true
        r.Name = stepErr.Name
        r.ErrorType = stepErr.ErrorType
    }
    var opErr *durable.OperationError
    if errors.As(err, &opErr) {
        r.MatchedOperationError = true
    }
    return r, nil
}

func main() { durable.Start(handler) }

errors.As(err, &opErr) against *OperationError matches any operation failure. The dotted edges in the diagram show these errors.As matches. Read opErr.Name, opErr.ErrorType, and opErr.Message. The wrapped Err is a stand-in rebuilt from the record, so matching the body's own error type never succeeds.

The three callback error types embed CallbackError, so one errors.As against *CallbackError matches an external failure, a timeout, and a submitter failure. StepInterruptedError and StepError are separate types. See Step interrupted. SerdesError, ClientError, and CheckpointError are not operation errors, so a bare value of one does not match *OperationError. A step serdes failure surfaces as a *StepError whose ErrorType is "SerdesError". Sentinels such as ErrCallbackTimedOut, ErrInvokeTimedOut, ErrWrongGoroutine, and ErrWrongContext match with errors.Is.

graph TD
  DEE[DurableExecutionException]
  DEE --> NonDeterministicExecutionException
  DEE --> StepException
  StepException --> StepInterruptedException
  DEE --> ChildContextException
  DEE --> InvokeException
  InvokeException --> InvokeFailedException
  InvokeException --> InvokeTimedOutException
  InvokeException --> InvokeStoppedException
  DEE --> CallbackException
  CallbackException --> CallbackFailedException
  CallbackException --> CallbackTimeoutException
  CallbackException --> CallbackSubmitterException
  DEE --> ParallelException
  DEE --> MapException
  DEE --> WaitForConditionException
  OCE["OperationCanceledException (System)"]
using Amazon.Lambda.DurableExecution;

// DurableExecutionException                — base class for all SDK exceptions
//   NonDeterministicExecutionException     — non-deterministic replay detected
//   StepException                          — step failed after retries exhausted
//     StepInterruptedException             — at-most-once step interrupted mid-attempt
//   ChildContextException                  — child context's user function failed
//   InvokeException                        — chained invoke failed
//     InvokeFailedException                — invoked function ran and threw (FAILED)
//     InvokeTimedOutException              — invoked function reached TIMED_OUT
//     InvokeStoppedException               — invocation stopped administratively
//   CallbackException                      — parent class for callback failures
//     CallbackFailedException              — external system reported a failure
//     CallbackTimeoutException             — callback (or heartbeat) timeout elapsed
//     CallbackSubmitterException           — submitter step failed after retries
//   ParallelException                      — parallel exceeded failure tolerance
//   MapException                           — map exceeded failure tolerance
//   WaitForConditionException              — wait-for-condition hit max attempts
//
// OperationCanceledException (System) — NOT a DurableExecutionException. Thrown
//   when the linked CancellationToken trips (workflow shutdown or cooperative
//   short-circuit). Treated as cancellation, not a step failure.

DurableExecutionException is the base class for all SDK exceptions.

StepException is thrown when a step exhausts all retry attempts. Its ErrorType, ErrorData, and OriginalStackTrace properties carry the details of the original exception. StepInterruptedException is a subclass thrown when an at-most-once step started but Lambda was interrupted before the SDK checkpointed the result. See Step interrupted below.

CallbackException is the parent class for callback failures. CallbackFailedException reports a failure the external system sent, CallbackTimeoutException signals a callback (or heartbeat) timeout, and CallbackSubmitterException wraps a failure raised inside the submitter delegate. InvokeException (and its InvokeFailedException, InvokeTimedOutException, and InvokeStoppedException subclasses) reports chained-invoke failures.

OperationCanceledException is a standard .NET exception, not a DurableExecutionException. The SDK surfaces it when the linked CancellationToken trips (workflow shutdown or cooperative short-circuit); it is treated as cancellation, not a step failure.

Validation errors

The SDK does not retry validation errors. It reports a validation error when you pass an invalid argument to an SDK operation, such as a negative duration.

The SDK throws TypeError for invalid configuration values.

import {
  withDurableExecution,
  createRetryStrategy,
} from "@aws/durable-execution-sdk-js";

export const handler = withDurableExecution(async (event, context) => {
  // The SDK throws a TypeError for invalid configuration values.
  // For example, passing a negative delay to createRetryStrategy:
  try {
    const retryStrategy = createRetryStrategy({
      initialDelay: { seconds: -1 }, // invalid: negative delay
      maxAttempts: 3,
    });
  } catch (err) {
    if (err instanceof TypeError) {
      context.logger.error("Invalid SDK configuration", { message: (err as Error).message });
      return { error: "InvalidConfiguration" };
    }
    throw err;
  }
  return { status: "ok" };
});

The SDK raises ValidationError for invalid configuration values.

from aws_durable_execution_sdk_python.config import Duration
from aws_durable_execution_sdk_python.context import DurableContext
from aws_durable_execution_sdk_python.exceptions import ValidationError
from aws_durable_execution_sdk_python.execution import durable_execution


@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
    # The SDK raises ValidationError for invalid configuration values.
    # For example, passing a negative duration:
    try:
        duration = Duration(seconds=-1)  # invalid
    except ValidationError as e:
        context.logger.error("Invalid SDK configuration", extra={"message": str(e)})
        return {"error": "InvalidConfiguration", "message": str(e)}
    return {"status": "ok"}

The SDK throws IllegalArgumentException for invalid configuration values.

import java.time.Duration;
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;

public class ValidationErrorExample extends DurableHandler<Map<String, Object>, Map<String, Object>> {

    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, DurableContext context) {
        // The SDK throws IllegalArgumentException for invalid configuration values.
        // For example, passing a duration shorter than 1 second to context.wait():
        try {
            context.wait("short-wait", Duration.ofMillis(500)); // invalid: less than 1 second
        } catch (IllegalArgumentException e) {
            context.getLogger().error("Invalid SDK configuration: " + e.getMessage());
            return Map.of("error", "InvalidConfiguration", "message", e.getMessage());
        }
        return Map.of("status", "ok");
    }
}

A constructor such as NewRetryStrategy returns a plain error for an invalid configuration, and its Must form, such as MustNewRetryStrategy, panics. An operation such as Wait, Map, or Parallel returns a plain error for an invalid argument or option. Start and Wrap panic on an invalid handler option.

package main

import (
    "time"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

func handler(ctx durable.Context, _ any) (map[string]string, error) {
    // The SDK returns a plain error for invalid configuration values.
    // For example, passing a negative delay to NewRetryStrategy:
    _, err := durable.NewRetryStrategy(durable.RetryConfig{
        InitialDelay: -1 * time.Second, // invalid: negative delay
        MaxAttempts:  3,
    })
    if err != nil {
        ctx.Logger().Error("Invalid SDK configuration", "message", err.Error())
        return map[string]string{"error": "InvalidConfiguration"}, nil
    }
    return map[string]string{"status": "ok"}, nil
}

func main() { durable.Start(handler) }

The SDK throws ArgumentException (or ArgumentOutOfRangeException) for invalid configuration values.

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;

public class ValidationErrorExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, ValidationResult>(Workflow, input, context);

    private async Task<ValidationResult> Workflow(object input, IDurableContext ctx)
    {
        // The SDK throws ArgumentOutOfRangeException for invalid configuration
        // values. For example, passing a duration shorter than 1 second to
        // WaitAsync:
        try
        {
            await ctx.WaitAsync(TimeSpan.FromMilliseconds(500), name: "short-wait"); // invalid: less than 1 second
        }
        catch (ArgumentOutOfRangeException e)
        {
            ctx.Logger.LogError("Invalid SDK configuration: {Message}", e.Message);
            return new ValidationResult("InvalidConfiguration", e.Message);
        }
        return new ValidationResult("ok", null);
    }
}

public record ValidationResult(string Status, string? Message);

Step interrupted

When you configure a step with at-most-once semantics, the SDK runs the step body at most once per retry attempt. If Lambda is interrupted after the step body starts but before the SDK checkpoints the result, the SDK does not re-run the step on the next invocation. Instead, it throws a step-interrupted exception.

Use at-most-once semantics for operations with side effects that must not run more than once, such as charging a payment or sending a notification. When you catch a step-interrupted exception, check the external system to determine whether the operation succeeded before deciding how to proceed.

import {
  withDurableExecution,
  StepSemantics,
  StepError,
} from "@aws/durable-execution-sdk-js";

export const handler = withDurableExecution(async (event, context) => {
  try {
    const result = await context.step(
      "charge-payment",
      async (stepCtx) => {
        return chargePayment((event as { amount: number }).amount);
      },
      { semantics: StepSemantics.AtMostOncePerRetry },
    );
    return { status: "charged", result };
  } catch (err) {
    if (
      err instanceof StepError &&
      err.cause?.name === "StepInterruptedError"
    ) {
      // Lambda interrupted the step before the SDK checkpointed the result.
      // The SDK will not re-run the step on the next invocation. Check your
      // payment system to determine whether the charge succeeded.
      context.logger.warn("Payment step interrupted; check payment system");
      return { status: "unknown" };
    }
    throw err;
  }
});

function chargePayment(amount: number): { charged: number } {
  return { charged: amount };
}
from aws_durable_execution_sdk_python.config import StepConfig, StepSemantics
from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
from aws_durable_execution_sdk_python.exceptions import StepInterruptedError
from aws_durable_execution_sdk_python.execution import durable_execution


@durable_step
def charge_payment(step_context: StepContext, amount: float) -> dict:
    return charge_external_system(amount)


@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
    try:
        result = context.step(
            charge_payment(event["amount"]),
            config=StepConfig(step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY),
        )
        return {"status": "charged", "result": result}
    except StepInterruptedError:
        # The step started but Lambda was interrupted before the result was
        # checkpointed. The SDK will not re-run the step on the next invocation.
        # Inspect your payment system to determine whether the charge succeeded.
        context.logger.warning("Payment step interrupted — check payment system")
        return {"status": "unknown"}


def charge_external_system(amount: float) -> dict:
    return {"charged": amount}
import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.config.StepConfig;
import software.amazon.lambda.durable.config.StepSemantics;
import software.amazon.lambda.durable.exception.StepInterruptedException;

public class StepInterrupted extends DurableHandler<Map<String, Object>, Map<String, Object>> {

    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, DurableContext context) {
        StepConfig config = StepConfig.builder()
                .semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
                .build();
        try {
            Map<String, Object> result = context.step(
                    "charge-payment", Map.class,
                    stepCtx -> chargePayment((Double) event.get("amount")),
                    config);
            return Map.of("status", "charged", "result", result);
        } catch (StepInterruptedException e) {
            // The step started but Lambda was interrupted before the result was
            // checkpointed. The SDK will not re-run the step on the next invocation.
            // Inspect your payment system to determine whether the charge succeeded.
            context.getLogger().warn("Payment step interrupted — check payment system");
            return Map.of("status", "unknown");
        }
    }

    private Map<String, Object> chargePayment(double amount) {
        return Map.of("charged", amount);
    }
}

Use durable.WithSemantics(durable.AtMostOncePerRetry) for a step with side effects that shouldn't run more than once. The SDK counts an interrupted attempt as a failed attempt, so the step's retry strategy decides whether a new attempt runs. When no retry remains, the step returns a *StepError whose ErrorType is "StepInterruptedError". Match the rebuilt cause with errors.As(err, &interruptedErr). errors.As(err, &stepErr) matches every step failure, so it alone does not identify an interruption.

package main

import (
    "errors"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type paymentEvent struct {
    Amount int `json:"amount"`
}

type receipt struct {
    Charged int `json:"charged"`
}

type paymentResult struct {
    Status string   `json:"status"`
    Result *receipt `json:"result,omitempty"`
}

func handler(ctx durable.Context, event paymentEvent) (paymentResult, error) {
    r, err := durable.Step(ctx, "charge-payment", func(_ durable.StepContext) (receipt, error) {
        return chargePayment(event.Amount), nil
    }, durable.WithSemantics(durable.AtMostOncePerRetry))
    if err != nil {
        var interrupted *durable.StepInterruptedError
        if errors.As(err, &interrupted) {
            // Lambda interrupted the step before the SDK recorded an outcome,
            // and no retry remains. Check the payment system to learn whether
            // the charge happened.
            ctx.Logger().Warn("Payment step interrupted; check payment system")
            return paymentResult{Status: "unknown"}, nil
        }
        return paymentResult{}, err
    }
    return paymentResult{Status: "charged", Result: &r}, nil
}

func chargePayment(amount int) receipt {
    return receipt{Charged: amount}
}

func main() { durable.Start(handler) }
using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;

public class StepInterruptedExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<PaymentEvent, PaymentResult>(Workflow, input, context);

    private async Task<PaymentResult> Workflow(PaymentEvent input, IDurableContext ctx)
    {
        var config = new StepConfig
        {
            Semantics = StepSemantics.AtMostOncePerRetry,
        };
        try
        {
            Charge result = await ctx.StepAsync(
                async (_, _) => ChargePayment(input.Amount),
                name: "charge-payment",
                config: config);
            return new PaymentResult("charged", result);
        }
        catch (StepInterruptedException)
        {
            // The step started but Lambda was interrupted before the result was
            // checkpointed. The SDK will not re-run the step on the next invocation.
            // Inspect your payment system to determine whether the charge succeeded.
            ctx.Logger.LogWarning("Payment step interrupted — check payment system");
            return new PaymentResult("unknown", null);
        }
    }

    private static Charge ChargePayment(double amount) => new(Charged: amount);
}

public record PaymentEvent(double Amount);
public record Charge(double Charged);
public record PaymentResult(string Status, Charge? Result);

Serialization errors

The SDK serializes step results to checkpoint storage. The default serializer handles standard types for each language. Custom serializers should throw the appropriate exception type when they encounter a value they cannot handle.

When a custom Serdes implementation throws during serialize or deserialize, the SDK throws SerdesFailedError as an unhandled exception. The durable functions backend retries the invocation.

import { withDurableExecution, Serdes, SerdesContext } from "@aws/durable-execution-sdk-js";

// A custom Serdes that throws on serialization failure.
// The SDK terminates the Lambda invocation when serialization fails.
const strictSerdes: Serdes<unknown> = {
  serialize: async (value: unknown, _context: SerdesContext) => {
    // Circular references cause JSON.stringify to throw
    return JSON.stringify(value);
  },
  deserialize: async (data: string | undefined, _context: SerdesContext) => {
    if (data === undefined) return undefined;
    return JSON.parse(data);
  },
};

export const handler = withDurableExecution(async (event, context) => {
  const result = await context.step(
    "build-result",
    async (stepCtx) => {
      // Return a value that can be serialized
      return { message: "hello", timestamp: Date.now() };
    },
    { serdes: strictSerdes },
  );
  return result;
});

A custom SerDes signals a permanent failure by raising SerDesError: the SDK returns a FAILED status response and does not retry. For a transient failure, such as an offloading SerDes whose network call times out, raise RetryableSerDesError instead: the SDK fails the invocation so the backend retries it, rather than surfacing the failure to your code.

from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
from aws_durable_execution_sdk_python.exceptions import SerDesError
from aws_durable_execution_sdk_python.execution import durable_execution


@durable_step
def build_result(step_context: StepContext) -> dict:
    # Return a value that can be serialized by the default serdes
    return {"message": "hello"}


@durable_execution
def lambda_handler(event: dict, context: DurableContext) -> dict:
    # SerDesError is raised when the SDK cannot serialize or deserialize a value.
    # The default serdes handles standard Python types. Custom serdes implementations
    # should raise SerDesError when they encounter a value they cannot handle.
    try:
        result = context.step(build_result())
        return result
    except SerDesError as e:
        context.logger.error("Serialization failed", extra={"message": str(e)})
        raise

When serialization or deserialization fails, the SDK throws SerDesException. The executor catches it, returns a FAILED status response, and does not retry.

import java.util.Map;
import software.amazon.lambda.durable.DurableContext;
import software.amazon.lambda.durable.DurableHandler;
import software.amazon.lambda.durable.exception.SerDesException;

public class SerdesErrorExample extends DurableHandler<Map<String, Object>, Map<String, Object>> {

    @Override
    public Map<String, Object> handleRequest(Map<String, Object> event, DurableContext context) {
        // SerDesException is thrown when the SDK cannot serialize or deserialize a value.
        // Custom SerDes implementations should throw SerDesException when they encounter
        // a value they cannot handle.
        try {
            Map<String, Object> result = context.step("build-result", Map.class, stepCtx ->
                    Map.of("message", "hello"));
            return result;
        } catch (SerDesException e) {
            context.getLogger().error("Serialization failed: " + e.getMessage());
            throw e;
        }
    }
}

A custom Serdes returns an error from Marshal or Unmarshal. By default the failure is permanent. The operation fails with a *SerdesError, and a step serdes failure surfaces as a *StepError whose ErrorType is "SerdesError". Return durable.RetryableSerdesError(err) to mark it transient so the backend re-invokes.

package main

import (
    "context"

    "github.com/aws/aws-durable-execution-sdk-go/durable"
)

type stepResult struct {
    Message string `json:"message"`
}

// customSerdes delegates to JSONSerdes. A serializer returns an error for a
// value it cannot handle. The SDK fails the operation with a
// *durable.SerdesError and does not retry it. Wrap the error with
// durable.RetryableSerdesError to mark the failure transient, so the backend
// invokes the execution again instead.
type customSerdes struct{}

func (customSerdes) Marshal(ctx context.Context, meta durable.SerdesContext, v any) ([]byte, error) {
    return durable.JSONSerdes.Marshal(ctx, meta, v)
}

func (customSerdes) Unmarshal(ctx context.Context, meta durable.SerdesContext, data []byte, v any) error {
    return durable.JSONSerdes.Unmarshal(ctx, meta, data, v)
}

func handler(ctx durable.Context, _ any) (stepResult, error) {
    return durable.Step(ctx, "build-result", func(_ durable.StepContext) (stepResult, error) {
        return stepResult{Message: "hello"}, nil
    }, durable.WithStepSerdes(customSerdes{}))
}

func main() { durable.Start(handler) }

The step result is serialized with the ILambdaSerializer registered on ILambdaContext.Serializer. When serialization or deserialization fails, the SDK surfaces the failure as an unhandled exception, returns a FAILED status, and does not retry.

using Amazon.Lambda.Core;
using Amazon.Lambda.DurableExecution;
using Microsoft.Extensions.Logging;

public class SerdesErrorExample
{
    public Task<DurableExecutionInvocationOutput> Handler(
        DurableExecutionInvocationInput input, ILambdaContext context)
        => DurableFunction.WrapAsync<object, ResultData>(Workflow, input, context);

    private async Task<ResultData> Workflow(object input, IDurableContext ctx)
    {
        // The step result is serialized to checkpoint storage with the
        // ILambdaSerializer registered on ILambdaContext.Serializer. When
        // serialization or deserialization fails, the SDK surfaces the failure as
        // an unhandled exception, returns a FAILED status, and does not retry.
        try
        {
            ResultData result = await ctx.StepAsync(
                async (_, _) => new ResultData("hello"),
                name: "build-result");
            return result;
        }
        catch (Exception e)
        {
            ctx.Logger.LogError("Serialization failed: {Message}", e.Message);
            throw;
        }
    }
}

public record ResultData(string Message);

See also