Skip to content

Temporal Error handling with Go: a roadmap ​

Why this matters, in 30 seconds ​

Temporal recovers from crashes, network failures and outages on its own. It cannot decide what a declined card, an out-of-stock item or a bug means for your business. That part is your code, and in Go it has sharp edges:

  • An error crosses a process boundary, so errors.Is and your own error types don't survive. Only a message, a Type string, a non-retryable flag and details do.
  • Wrapping a non-retryable error with fmt.Errorf silently makes it retryable.
  • A Workflow that returns an error fails for good, while one that panics stays stuck and keeps retrying.
  • Timeouts, Retry Policies and compensations decide whether a failure ends in a retry, a clean business error or an inconsistent order.

This guide covers those rules, chapter by chapter.


The roadmap ​

Stage 1: Foundations (core concepts, must-know) ​

#ChapterYou'll be able to…
01Platform failures vs application failuresTell which failures Temporal recovers from on its own and which need your code, and explain forward vs backward recovery.
02Temporal's error types in GoName the error types, know what survives the trip from an Activity to a Workflow, and classify any error with errors.As.
03Workflow Task failure vs Workflow Execution failurePredict whether a Workflow fails or gets stuck, use recover and defer safely, and find stuck Workflows.

Stage 2: Daily use (raising and handling errors) ​

#ChapterYou'll be able to…
04Raising application errorsReturn Application errors with a Type, details, a non-retryable flag, a custom retry delay or the Benign category.
05Handling errors in WorkflowsBranch on timeouts, Types, cancellation, panics and Child Workflow errors, fall back, and map errors to API responses.
06Wrapping errors without losing retryabilityAdd context to an error without making a non-retryable error retryable, and test it.

Stage 3: Advanced (retries and failure detection) ​

#ChapterYou'll be able to…
07Retry PoliciesRead and tune the defaults, compute retry intervals, list non-retryable Types, and decide when a Workflow should retry.
08Detecting failures with timeouts and HeartbeatsPick Activity timeouts as failure detectors, react to each timeout type, and replace Workflow timeouts with Timers.

Stage 4: Production ​

#ChapterYou'll be able to…
09Backward recovery and an error strategyClassify failures, write a Saga with compensations, inspect failing Activities, and keep error text out of plain sight.

Core concepts vs. implementation details ​

Core concepts: understand these firstImplementation details, options & sub-features: learn when needed
Platform vs application failures, forward vs backward recovery (01)—
Errors are converted at the boundary; only message, Type, flag and details survive (02)Type names from getErrType, Is* helpers, ServerError, NexusOperationError (02)
Return → Execution failure; panic → Task failure (03)WorkflowPanicPolicy, recover/defer rules, TemporalReportedProblems (03)
ApplicationError with a Type (04)Details, NextRetryDelay, Benign category (04)
Check the wrapper, then the cause (05)RetryState, Child Workflow errors, Client-side mapping, tests (05)
Only the outermost error counts (06)A wrap helper that keeps the flag (06)
Activities retry by default, Workflows don't (07)Interval formula, NonRetryableErrorTypes, Workflow Retry Policy (07)
Start-To-Close is the crash detector (08)Schedule-To-Start, Heartbeat timeout cap, Workflow timeouts (08)
Compensation for backward recovery (09)Disconnected contexts, Pending Activities, Failure Converter encryption (09)

Which error handling do I need? ​

text
Where does the failure come from?
├── Worker crash, network, Service outage    → nothing to write, Temporal retries       → chapter 01
└── Your code (bad input, business rule, external API)
    ├── Will retrying help?
    │   ├── Yes, soon                         → return a plain/retryable error           → chapters 04, 07
    │   ├── Yes, but not before X             → NextRetryDelay                           → chapter 04
    │   └── No                                → NewNonRetryableApplicationError          → chapter 04
    │                                           (or NonRetryableErrorTypes on the caller) → chapter 07
    ├── Adding context to an error?           → keep the flag: wrap with ApplicationErrorOptions → chapter 06
    └── Expected and frequent (e.g. "not ready yet")?  → Category: Benign               → chapter 04

In the Workflow, after an Activity fails:
├── Timed out (TimeoutError or RetryState Timeout)? → check this first                  → chapters 05, 08
├── Known Type?                               → switch on ApplicationError.Type()       → chapter 05
├── Earlier steps must be undone?             → compensate (Saga)                       → chapter 09
└── Nothing to do?                            → return a non-retryable ApplicationError → chapters 03, 05

Workflow "Running" but not progressing?       → Workflow Task failure (panic, non-determinism,
                                                bad Activity options)                    → chapters 03, 07, 08

Go API cheat sheet ​

I want to…Go APIChapter
Return a business errortemporal.NewApplicationError(msg, errType, details...)04
Stop retries from the Activitytemporal.NewNonRetryableApplicationError(msg, errType, cause, details...)04
Set delay, cause, categorytemporal.NewApplicationErrorWithOptions(msg, errType, temporal.ApplicationErrorOptions{…})04, 06
Mark an expected errorApplicationErrorOptions{Category: temporal.ApplicationErrorCategoryBenign}04
Read Type and detailserrors.As(err, &appErr), appErr.Type(), appErr.NonRetryable(), appErr.Details(&v)02, 05
Know why retries stoppederrors.As(err, &actErr), actErr.RetryState()02, 05
Detect a timeouterrors.As(err, &timeoutErr), timeoutErr.TimeoutType(), temporal.IsTimeoutError(err)05, 08
Get the last Heartbeat detailstimeoutErr.HasLastHeartbeatDetails(), timeoutErr.LastHeartbeatDetails(&v)08
Detect cancellation / panictemporal.IsCanceledError(err), temporal.IsPanicError(err)02, 05
Handle a Child Workflow failureerrors.As(err, &childErr) (*temporal.ChildWorkflowExecutionError)05
Configure retriesworkflow.ActivityOptions{RetryPolicy: &temporal.RetryPolicy{InitialInterval, BackoffCoefficient, MaximumInterval, MaximumAttempts, NonRetryableErrorTypes}}07
Retry a whole Workflowclient.StartWorkflowOptions{RetryPolicy: …}07
Fail Workflows on panicworker.Options{WorkflowPanicPolicy: worker.FailWorkflow}03
Find stuck WorkflowsTemporalReportedProblems IN ("category=WorkflowTaskFailed")03
Run compensations after cancelworkflow.NewDisconnectedContext(ctx)09
Inspect failing Activitiesc.DescribeWorkflowExecution(ctx, id, "") → PendingActivities09
Encrypt error messagestemporal.NewDefaultFailureConverter(temporal.DefaultFailureConverterOptions{EncodeCommonAttributes: true, …}) + a codec09

About the code samples ​

  • Each chapter is a self-contained module named github.com/acme/shop. Every Go block starts with // file: <path> so you can paste it into that path.
  • All samples were compiled, gofmt-checked and go vet-checked against Go SDK v1.50.0 (go.temporal.io/sdk). The tests in chapters 05 and 06 pass.
  • Every behavior the chapters state was run on a dev server (Temporal CLI 1.9.1, Server 1.32.0): how each kind of Go error arrives in the Workflow, returned errors vs panics, recover in Workflow code, wrapping, NextRetryDelay, Benign logging, NonRetryableErrorTypes, Workflow retries, every Activity timeout, invalid Activity options, TemporalReportedProblems, the checkout Saga and the encrypted Failure Converter.
  • External systems are interfaces or fakes. Run everything locally with temporal server start-dev (UI at http://localhost:8233).

Notes from checking the docs against the SDK ​

  • A returned error fails the Workflow in Go. The Encyclopedia says a non-Temporal error fails the Workflow Task and is retried. In Go, a Workflow that returns any error, even errors.New, fails the Execution (status Failed). Only a panic or non-determinism fails the Task (chapter 03).
  • recover() works in Workflow code. The Go page says you cannot recover() from a panic in a defer. It works. What fails is a deferred function that calls a blocking SDK API (like workflow.Sleep) during the panic: the Task fails with "yield during panic unwinding" (chapter 03).
  • Type names of plain Go errors. The Encyclopedia says the error's type name is preserved. errors.New arrives with an empty Type, fmt.Errorf("%w") with "wrapError", and only your own struct types keep a useful name. errors.Is and errors.As on your own types are false in the Workflow (chapter 02).
  • The outermost-error rule isn't on the Go page. The Encyclopedia warns that fmt.Errorf wrapping makes a non-retryable error retryable; the Go page doesn't. Verified: the wrapped error was retried 3 times with Type wrapError. The Benign category follows the same rule (chapter 06).
  • Go supports per-error retry delays. The Retry Policies page lists Java, Rust and TypeScript for "next Retry delay". Go has ApplicationErrorOptions.NextRetryDelay (a 3s delay overrode a 200ms InitialInterval) (chapter 04).
  • A timeout isn't always a TimeoutError. When a Workflow Execution Timeout cuts Activity retries short, the Workflow sees ActivityError → ApplicationError with RetryState Timeout. Check timeouts first (chapters 02, 05, 08).
  • Schedule-To-Close gives up early. When the next retry can't fit, the Service stops with "Not enough time to schedule next retry". The TimeoutError wraps the last attempt's error (chapter 08).
  • Workflow Execution Timeout's type. The Client gets a TimeoutError with TimeoutType()StartToClose, and deferred cleanup never runs (chapter 08).
  • Invalid Activity options don't return an error. A negative MaximumAttempts, or no Start-To-Close and no Schedule-To-Close, fails the Workflow Task with BadScheduleActivityAttributes and the Workflow stays Running. The page says a negative value "results in an error" (chapters 07, 08).
  • Workflow Task Timeout cap. The SDK doc comment says 1 minute. The Server caps it at 120s (a 5m value was stored as 120s), as the Encyclopedia says (chapter 08).
  • TemporalReportedProblems without extra config. The Web UI page says the dynamic config must be updated. On a dev server it was set after 5 failed Workflow Tasks with no config change (chapter 03).
  • Failure Converter. EncodeCommonAttributes: true without a codec only moves the text into a readable payload; a codec is what encrypts it. Go ApplicationErrors carry no stack trace, only panics do (chapter 09).
  • Test environment vs Service. TestWorkflowEnvironment reports RETRY_STATE_UNSPECIFIED when retries run out; the Service reports MaximumAttemptsReached. Branch on Types, not on RetryState values that tests can't reproduce (chapter 05).
  • The Go page's sample doesn't compile. It uses undefined outcome, failure and YourActivity. Chapter 05 has a complete version.

Sources ​

Encyclopedia: Failures and error handling · Application failures · Detecting Activity failures · Detecting Workflow failures · Retry Policies

Go SDK: Error handling