Error Handling

Error structure

All errors in Google Cloud Workflows are represented as maps:

{
  "message": "Human-readable error description",
  "code": 404,
  "tags": ["HttpError"]
}

HTTP errors include additional fields:

{
  "message": "HTTP request failed with status 404",
  "code": 404,
  "tags": ["HttpError"],
  "headers": {"content-type": "application/json"},
  "body": {"error": "not found"}
}

try / except

Catch errors and handle them:

- safe_call:
    try:
      steps:
        - fetch:
            call: http.get
            args:
              url: http://localhost:9090/data
            result: response
    except:
      as: e
      steps:
        - check_error:
            switch:
              - condition: ${"ConnectionFailedError" in e.tags}
                return: "Service is not running"
              - condition: ${"HttpError" in e.tags and e.code == 404}
                return: null
              - condition: true
                raise: ${e}

The error variable (e in the as field) is a map with message, code, and tags fields.

try / retry / except

Add retry with exponential backoff:

- resilient_call:
    try:
      steps:
        - fetch:
            call: http.get
            args:
              url: http://localhost:9090/data
            result: response
    retry:
      predicate: ${http.default_retry}
      max_retries: 5
      backoff:
        initial_delay: 1
        max_delay: 60
        multiplier: 2
    except:
      as: e
      steps:
        - handle:
            return: '${"Failed after retries: " + e.message}'

Key behavior: Retries re-execute the entire try block from the beginning, not just the failed step.

Retry count: max_retries: 3 means 1 initial attempt + 3 retries = 4 total attempts.

Backoff formula: delay = min(initial_delay * multiplier^attempt, max_delay)

Error tags

The emulator supports all 17 Google Cloud Workflows error tags:

TagWhen raisedTypical code
AuthErrorGenerating credentials fails0
ConnectionErrorConnection broke mid-transfer0
ConnectionFailedErrorConnection never established (service down, DNS failure)0
HttpErrorNon-2xx HTTP responseHTTP status code
IndexErrorList index out of range0
KeyErrorMap key not found, or unknown env var in sys.get_env0
OperationErrorLong-running operation failure0
ParallelNestingErrorParallel nesting exceeds depth 20
RecursionErrorCall stack depth exceeds 200
ResourceLimitErrorMemory, step count, or other resource limits exceeded0
ResponseTypeErrorUnexpected response type from operation0
SystemErrorInternal system error0
TimeoutErrorHTTP request or callback await timed out0
TypeErrorType mismatch (e.g., "hi" + 5, not "string")0
UnhandledBranchErrorRaised after continueAll parallel when branches had errors0
ValueErrorCorrect type but invalid value (e.g., int("abc"))0
ZeroDivisionErrorDivision or modulo by zero0

Errors can have multiple tags. For example, an HTTP 404 error has tags: ["HttpError"].

ConnectionFailedError vs ConnectionError

This distinction is critical for local development:

ErrorMeaningCommon cause
ConnectionFailedErrorConnection was never establishedService not running, port not listening, DNS failure
ConnectionErrorConnection established but broke during transferService crashed mid-response

When your local service is not running, the emulator raises ConnectionFailedError. This is the error you'll see most often during development.

Catching errors by tag

- step:
    try:
      call: http.get
      args:
        url: http://localhost:9090/api
      result: response
    except:
      as: e
      steps:
        - route_error:
            switch:
              - condition: ${"ConnectionFailedError" in e.tags}
                return: "Service is down"
              - condition: ${"HttpError" in e.tags and e.code == 429}
                next: retry_later
              - condition: ${"HttpError" in e.tags and e.code >= 500}
                return: "Server error"
              - condition: ${"TimeoutError" in e.tags}
                return: "Request timed out"
              - condition: true
                raise: ${e}

raise

Throw a custom error:

# String error -> {message: "...", code: 0, tags: []}
- fail:
    raise: "validation failed"

# Map error with custom fields
- fail:
    raise:
      code: 400
      message: "Invalid order ID"
      tags: ["ValidationError"]

# Re-raise a caught error
- rethrow:
    raise: ${e}

Error propagation

  1. Error occurs in a step
  2. If inside a try block with retry: retry is attempted first
  3. If retry exhausted or not configured: except block executes (if present)
  4. If no except or except re-raises: error propagates up
  5. In a subworkflow: propagates to the caller
  6. In a parallel branch: depends on exception policy (unhandled aborts all; continueAll collects)
  7. At the top level of main: execution fails with state FAILED

Variable scoping with try/except

Variables declared inside except are not visible outside:

# WRONG: error_msg is not accessible after the try/except block
- handle:
    try:
      call: http.get
      args:
        url: http://localhost:9090/data
      result: response
    except:
      as: e
      steps:
        - save:
            assign:
              - error_msg: ${e.message}   # Only in scope inside except

# CORRECT: declare the variable before the try/except
- init:
    assign:
      - error_msg: null
- handle:
    try:
      call: http.get
      args:
        url: http://localhost:9090/data
      result: response
    except:
      as: e
      steps:
        - save:
            assign:
              - error_msg: ${e.message}   # Modifies parent-scope variable
- use:
    return: ${error_msg}                  # Works

Built-in retry policies

PolicyRetries onDoes NOT retry
http.default_retry429, 502, 503, 504, ConnectionError, TimeoutError500
http.default_retry_non_idempotentSame as above500
retry.alwaysEverything(nothing)
retry.never(nothing)Everything

retry.default_backoff: initial_delay 1s, max_delay 60s, multiplier 1.25.

See the Standard Library > Custom retry predicates section for writing predicates that retry on HTTP 500.