Files
temporalio__skill-temporal-…/references/go/error-handling.md
Donald Pinckney b5719bc143 PR Tracking Initial Release (#4)
* Add initial skill for testing, which is simply Steve's skill (#1)

* Add initial skill for testing, which is simply Steve's skill

* Rename skill to 'temporal-dev' and update version

Updated skill name and version for Temporal Python.

* Use claude to merge Steve's, Max's, and Mason's skills.  (#2)

* Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills

* Add missing things from Steve

* trigger tweaks

* Add in common gotchas from Johann

* add simple feedback mechanism (#3)

* Change skill name to kebab-case, for compatibility with Amp and Cline (#7)

* Clean up references/core/ai-integration.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/determinism.md

* Clean up references/core/determinism.md

* Update error-reference.md

* Update interactive-workflows.md

* Clean up patterns.md

* Cut shell scripts

* Edit troubleshooting.md

* remove interceptors for now

* remove dynamic workflows

* clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals

* Improve references/python/advanced-features.md

* Use explicit namespace in connect

* remove duplicated content from determinism.md, clean up

* Improve references/python/data-handling.md

* Prefer start_to_close_timeout

* don't explicitely provide defaults for retry policies

* error-handling.md cleanup

* move idempotency patterns to patterns.md

* remove multi-param activities

* small edits

* Unify sandbox stuff into one file

* local activities aren't experimental

* Clean up references/python/sync-vs-async.md

* Cleanup observability.md, remove duplicated search attributes

* Cut otel for now

* cut a lot of duplicate stuff from python gotchas, address comments

* de-duplicate content

* Lots of improvements to testing

* cleanup to top level of skill (like CLI install instructions), and to top-level of python

* Improve patterns.md

* clean up ai-patterns.md

* Update readme with installation instructions

* remove ts directory

* De-couple core from python and TypeScript as much as possible

* Remove TypeScript hints

* add prompting for feedback at startup - wait for ethan on slack channel

* shorten url

* Update slack channel

* Automated pass over on python cleanup & deduplication

* Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34)

* Add TypeScript (#31)

Adds initial support for TypeScript to the skill

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>

* Fix typos and reference links (#36)

* Fix typos and reference links

* 2 more typo fixes

* quick edit to readme (#37)

* Fix saga compensations to run under cancellation protection (#43)

When a workflow is cancelled mid-saga, compensations must run in a
cancellation-protected scope, otherwise they are immediately cancelled
before they can execute.

- Python: wrap compensation loop in asyncio.shield() so it runs even
  when the workflow receives a CancelledError
- TypeScript: wrap compensation loop in CancellationScope.nonCancellable()
  so it runs even when the root scope is cancelled (per official docs:
  "Cleanup logic must be in a nonCancellable scope")
- TypeScript: also fix compensation registration order — register BEFORE
  calling the activity (was already correct in Python)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Update readme for public preview (#45)

* a few more readme tweaks (#46)

* Add MIT License to the project (#47)

* Add Go (supersedes other PR) (#38)

* progress on go

* Go translation workflow completed.

* missed a few spots

* Manual edits

* Address feedback

* Add gotcha about anonymous local activities

* Sample code for payload converter

* clarify sdk protection mechanisms

* Setup CODEOWNERS to AI SDK team (#48)

* Align version number in SKILL.md and plugin.json. (#49)

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:36:15 -04:00

5.6 KiB

Go SDK Error Handling

Overview

The Go SDK uses error return values (not exceptions). All Temporal errors implement the error interface. Activity errors returned to workflows are wrapped in *temporal.ActivityError; use errors.As to unwrap them.

Application Errors

import "go.temporal.io/sdk/temporal"

func ValidateOrder(ctx context.Context, order Order) error {
	if !order.IsValid() {
		return temporal.NewApplicationError(
			"Invalid order",
			"ValidationError",
		)
	}
	return nil
}

temporal.NewApplicationError(message, errType, details...) creates a retryable *temporal.ApplicationError. Use NewApplicationErrorWithCause to include a wrapped cause.

Non-Retryable Errors

func ChargeCard(ctx context.Context, input ChargeCardInput) (string, error) {
	if !isValidCard(input.CardNumber) {
		return "", temporal.NewNonRetryableApplicationError(
			"Permanent failure - invalid credit card",
			"PaymentError",
			nil, // cause
		)
	}
	return processPayment(input.CardNumber, input.Amount)
}

temporal.NewNonRetryableApplicationError(message, errType, cause, details...) is always non-retryable regardless of RetryPolicy. You can also mark error types as non-retryable in the RetryPolicy instead:

RetryPolicy: &temporal.RetryPolicy{
	NonRetryableErrorTypes: []string{"PaymentError", "ValidationError"},
},

Handling Activity Errors in Workflows

import (
	"errors"

	"go.temporal.io/sdk/temporal"
	"go.temporal.io/sdk/workflow"
)

func MyWorkflow(ctx workflow.Context) (string, error) {
	var result string
	err := workflow.ExecuteActivity(ctx, RiskyActivity).Get(ctx, &result)
	if err != nil {
		var applicationErr *temporal.ApplicationError
		if errors.As(err, &applicationErr) {
			switch applicationErr.Type() {
			case "ValidationError":
				// handle validation error
			case "PaymentError":
				// handle payment error
			default:
				// handle unknown error type
			}
		}

		var timeoutErr *temporal.TimeoutError
		if errors.As(err, &timeoutErr) {
			switch timeoutErr.TimeoutType() {
			case enumspb.TIMEOUT_TYPE_START_TO_CLOSE:
				// handle start-to-close timeout
			case enumspb.TIMEOUT_TYPE_HEARTBEAT:
				// handle heartbeat timeout
			}
		}

		var canceledErr *temporal.CanceledError
		if errors.As(err, &canceledErr) {
			// handle cancellation
		}

		var panicErr *temporal.PanicError
		if errors.As(err, &panicErr) {
			// panicErr.Error() and panicErr.StackTrace()
		}

		return "", err
	}
	return result, nil
}

Retry Configuration

import (
	"time"

	"go.temporal.io/sdk/temporal"
	"go.temporal.io/sdk/workflow"
)

func MyWorkflow(ctx workflow.Context) error {
	ao := workflow.ActivityOptions{
		StartToCloseTimeout: 10 * time.Minute,
		RetryPolicy: &temporal.RetryPolicy{
			InitialInterval:        time.Second,
			BackoffCoefficient:     2.0,
			MaximumInterval:        time.Minute,
			MaximumAttempts:        5,
			NonRetryableErrorTypes: []string{"ValidationError", "PaymentError"},
		},
	}
	ctx = workflow.WithActivityOptions(ctx, ao)
	return workflow.ExecuteActivity(ctx, MyActivity).Get(ctx, nil)
}

Only set options such as MaximumInterval, MaximumAttempts, etc. if you have a domain-specific reason to. If not, prefer to leave them at their defaults.

Timeout Configuration

ao := workflow.ActivityOptions{
	StartToCloseTimeout:    5 * time.Minute,  // Single attempt max duration
	ScheduleToCloseTimeout: 30 * time.Minute, // Total time including retries
	ScheduleToStartTimeout: 10 * time.Minute, // Time waiting in task queue
	HeartbeatTimeout:       2 * time.Minute,  // Between heartbeats
}
ctx = workflow.WithActivityOptions(ctx, ao)
  • StartToCloseTimeout: Max time for a single Activity Task Execution. Prefer this over ScheduleToCloseTimeout.
  • ScheduleToCloseTimeout: Total time including retries.
  • ScheduleToStartTimeout: Time an Activity Task can wait in the Task Queue before a Worker picks it up. Rarely needed.
  • HeartbeatTimeout: Max time between heartbeats. Required for long-running activities to detect failures.

Either StartToCloseTimeout or ScheduleToCloseTimeout must be set.

Workflow Failure

Returning any error from a workflow function fails the execution. Return nil for success.

Important Go-specific behavior: In the Go SDK, returning any error from a workflow fails the workflow execution by default — there is no automatic retry. This differs from other SDKs (Python, TypeScript) where non-ApplicationError exceptions cause the workflow task to retry indefinitely. In Go, if you want workflow-level retries, you must explicitly set a RetryPolicy on the StartWorkflowOptions.

func MyWorkflow(ctx workflow.Context) (string, error) {
	if someCondition {
		return "", temporal.NewApplicationError(
			"Cannot process order",
			"BusinessError",
		)
	}
	return "success", nil
}

To prevent workflow retry, return a non-retryable error:

return "", temporal.NewNonRetryableApplicationError(
	"Unrecoverable failure",
	"FatalError",
	nil,
)

Note: If an activity returns a non-retryable error, the workflow receives an *temporal.ActivityError wrapping it. To fail the workflow without retry, wrap it in a new NewNonRetryableApplicationError.

Best Practices

  1. Use specific error types for different failure modes
  2. Mark permanent failures as non-retryable
  3. Set appropriate timeouts; prefer StartToCloseTimeout over ScheduleToCloseTimeout
  4. Let Temporal handle retries via RetryPolicy rather than implementing retry logic yourself
  5. Use errors.As to unwrap and inspect specific error types
  6. Design activities to be idempotent for safe retries (see references/core/patterns.md)