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.Isand 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.Errorfsilently 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)
| # | Chapter | You'll be able to… |
|---|---|---|
| 01 | Platform failures vs application failures | Tell which failures Temporal recovers from on its own and which need your code, and explain forward vs backward recovery. |
| 02 | Temporal's error types in Go | Name the error types, know what survives the trip from an Activity to a Workflow, and classify any error with errors.As. |
| 03 | Workflow Task failure vs Workflow Execution failure | Predict whether a Workflow fails or gets stuck, use recover and defer safely, and find stuck Workflows. |
Stage 2: Daily use (raising and handling errors)
| # | Chapter | You'll be able to… |
|---|---|---|
| 04 | Raising application errors | Return Application errors with a Type, details, a non-retryable flag, a custom retry delay or the Benign category. |
| 05 | Handling errors in Workflows | Branch on timeouts, Types, cancellation, panics and Child Workflow errors, fall back, and map errors to API responses. |
| 06 | Wrapping errors without losing retryability | Add context to an error without making a non-retryable error retryable, and test it. |
Stage 3: Advanced (retries and failure detection)
| # | Chapter | You'll be able to… |
|---|---|---|
| 07 | Retry Policies | Read and tune the defaults, compute retry intervals, list non-retryable Types, and decide when a Workflow should retry. |
| 08 | Detecting failures with timeouts and Heartbeats | Pick Activity timeouts as failure detectors, react to each timeout type, and replace Workflow timeouts with Timers. |
Stage 4: Production
| # | Chapter | You'll be able to… |
|---|---|---|
| 09 | Backward recovery and an error strategy | Classify 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 first | Implementation 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, 08Go API cheat sheet
| I want to… | Go API | Chapter |
|---|---|---|
| Return a business error | temporal.NewApplicationError(msg, errType, details...) | 04 |
| Stop retries from the Activity | temporal.NewNonRetryableApplicationError(msg, errType, cause, details...) | 04 |
| Set delay, cause, category | temporal.NewApplicationErrorWithOptions(msg, errType, temporal.ApplicationErrorOptions{…}) | 04, 06 |
| Mark an expected error | ApplicationErrorOptions{Category: temporal.ApplicationErrorCategoryBenign} | 04 |
| Read Type and details | errors.As(err, &appErr), appErr.Type(), appErr.NonRetryable(), appErr.Details(&v) | 02, 05 |
| Know why retries stopped | errors.As(err, &actErr), actErr.RetryState() | 02, 05 |
| Detect a timeout | errors.As(err, &timeoutErr), timeoutErr.TimeoutType(), temporal.IsTimeoutError(err) | 05, 08 |
| Get the last Heartbeat details | timeoutErr.HasLastHeartbeatDetails(), timeoutErr.LastHeartbeatDetails(&v) | 08 |
| Detect cancellation / panic | temporal.IsCanceledError(err), temporal.IsPanicError(err) | 02, 05 |
| Handle a Child Workflow failure | errors.As(err, &childErr) (*temporal.ChildWorkflowExecutionError) | 05 |
| Configure retries | workflow.ActivityOptions{RetryPolicy: &temporal.RetryPolicy{InitialInterval, BackoffCoefficient, MaximumInterval, MaximumAttempts, NonRetryableErrorTypes}} | 07 |
| Retry a whole Workflow | client.StartWorkflowOptions{RetryPolicy: …} | 07 |
| Fail Workflows on panic | worker.Options{WorkflowPanicPolicy: worker.FailWorkflow} | 03 |
| Find stuck Workflows | TemporalReportedProblems IN ("category=WorkflowTaskFailed") | 03 |
| Run compensations after cancel | workflow.NewDisconnectedContext(ctx) | 09 |
| Inspect failing Activities | c.DescribeWorkflowExecution(ctx, id, "") → PendingActivities | 09 |
| Encrypt error messages | temporal.NewDefaultFailureConverter(temporal.DefaultFailureConverterOptions{EncodeCommonAttributes: true, …}) + a codec | 09 |
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 andgo 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,
recoverin 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 cannotrecover()from a panic in adefer. It works. What fails is a deferred function that calls a blocking SDK API (likeworkflow.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.Newarrives with an empty Type,fmt.Errorf("%w")with"wrapError", and only your own struct types keep a useful name.errors.Isanderrors.Ason 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.Errorfwrapping makes a non-retryable error retryable; the Go page doesn't. Verified: the wrapped error was retried 3 times with TypewrapError. 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 seesActivityError→ApplicationErrorwithRetryStateTimeout. 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
TimeoutErrorwraps the last attempt's error (chapter 08). - Workflow Execution Timeout's type. The Client gets a
TimeoutErrorwithTimeoutType()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 withBadScheduleActivityAttributesand 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).
TemporalReportedProblemswithout 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: truewithout a codec only moves the text into a readable payload; a codec is what encrypts it. GoApplicationErrors carry no stack trace, only panics do (chapter 09). - Test environment vs Service.
TestWorkflowEnvironmentreportsRETRY_STATE_UNSPECIFIEDwhen retries run out; the Service reportsMaximumAttemptsReached. Branch on Types, not onRetryStatevalues that tests can't reproduce (chapter 05). - The Go page's sample doesn't compile. It uses undefined
outcome,failureandYourActivity. 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