Skip to main content

Error handling

View Markdown

Error handling​

Within a Workflow, an Activity or Child Workflow execution might fail. You can handle errors differently based on the error type.

If the Activity returns an error as errors.New() or fmt.Errorf(), that error is converted into *temporal.ApplicationError.

If the Activity returns an error as temporal.NewNonRetryableApplicationError("error message", "ErrorType", cause, details), that error is returned as *temporal.ApplicationError.

There are other types of errors such as *temporal.TimeoutError, *temporal.CanceledError and *temporal.PanicError.

Here's an example of handling Activity errors within Workflow code that differentiates between different error types.

go-sandbox/bestpractices/errorhandling/workflow.go

err := workflow.ExecuteActivity(ctx, YourActivity, failure).Get(ctx, nil)
if err != nil {
var applicationErr *temporal.ApplicationError
if errors.As(err, &applicationErr) {
// retrieve error message
workflow.GetLogger(ctx).Info("Application error", "error", applicationErr.Error())

// handle Activity errors (created via NewApplicationError() API)
var detailMsg string // assuming Activity return error by NewApplicationError("message", "ErrType", "string details")
if err := applicationErr.Details(&detailMsg); err != nil { // extract strong typed details
workflow.GetLogger(ctx).Warn("Unable to decode error details", "error", err)
}

// handle Activity errors (errors created other than using NewApplicationError() API)
switch applicationErr.Type() {
case "CustomErrTypeA":
// handle CustomErrTypeA
outcome = "CustomErrTypeA: " + detailMsg
case CustomErrTypeB:
// handle CustomErrTypeB
outcome = "CustomErrTypeB: " + detailMsg
default:
// newer version of Activity could return new errors that Workflow was not aware of.
outcome = "unknown application error"
}
}

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

var timeoutErr *temporal.TimeoutError
if errors.As(err, &timeoutErr) {
// handle timeout, could check timeout type by timeoutErr.TimeoutType()
switch timeoutErr.TimeoutType() {
case enumspb.TIMEOUT_TYPE_SCHEDULE_TO_START:
// Handle ScheduleToStart timeout.
outcome = "ScheduleToStart timeout"
case enumspb.TIMEOUT_TYPE_START_TO_CLOSE:
// Handle StartToClose timeout.
outcome = "StartToClose timeout"
case enumspb.TIMEOUT_TYPE_HEARTBEAT:
// Handle heartbeat timeout.
outcome = "Heartbeat timeout"
default:
}
}

var panicErr *temporal.PanicError
if errors.As(err, &panicErr) {
// handle panic, message and call stack are available by panicErr.Error() and panicErr.StackTrace()
outcome = "panic"
}
}

Panics and deferred functions​

In Go, defer schedules cleanup functions and recover() catches panics to prevent them from crashing the program. This doesn't work the same way in Temporal Workflow code — you cannot recover() from a panic inside a defer. Deferred functions that try to interact with the Temporal SDK during panic unwinding will re-panic immediately.

Use defer only for local cleanup. Handle Temporal API cleanup through explicit error checks instead.