new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Temporal · all subjects

failure handling

24 notes, read out of this brain and free to use. Each one was extracted from a source and is re-checked against its exam.

Handling CanceledError in Go Workflows

Use errors.As(err, &canceledErr) to check for *CanceledError and handle cancellation accordingly.

Handling PanicError in Go Workflows

Use errors.As(err, &panicErr) to check for *PanicError. Call panicErr.Error() to get the panic message and panicErr.StackTrace() to retrieve the call stack.

Defer and recover behavior in Temporal Go Workflows

In Temporal Workflow code, defer schedules cleanup functions but recover() cannot catch panics 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.

Never catch Throwable or Error in Workflow or Activity code

Workflow and Activity code should only ever catch Exception or a narrower type. Never catch Throwable or Error. The Java SDK uses subclasses of Error as internal control signals that must reach the SDK's own code uncaught. DestroyWorkflowThreadError interrupts a Workflow thread so the Worker can release it back to the pool, for example when the Workflow Execution is evicted from the Worker's cache; if Workflow code catches it, the thread doesn't unwind and eviction can stall. UnsupportedVersion is thrown by Workflow.getVersion() when replayed history was produced by code outside the version range the current Workflow code declares, and extends Error specifically so that application code won't catch it by mistake.

Do not swallow CanceledFailure in Workflow code

CanceledFailure should be rethrown, optionally after cleanup in a detached Cancellation Scope. Do not swallow it: cancellation is cooperative, and swallowing it lets the Workflow Execution finish as 'Completed' instead of 'Canceled.' Swallowing CanceledFailure with a broad catch causes the canceled Workflow Execution to report 'Completed' instead of 'Canceled.'

Never swallow failures with a broad catch in Workflow code

A catch (Throwable t) or catch (Exception e) that only logs and returns placed around Workflow logic causes three separate problems: DestroyWorkflowThreadError and UnsupportedVersion are swallowed instead of reaching the SDK, which can stall Worker cache eviction and interfere with replay; CanceledFailure is swallowed so a canceled Workflow Execution reports 'Completed' instead of 'Canceled'; every other exception including real bugs disappears with only a log line instead of failing the Workflow Task or Workflow Execution, so there's no signal in the Event History that anything went wrong.

Catch pattern for Workflow exception handling

Use this rule of thumb when deciding what to catch in a Workflow, Update, or Signal handler: (1) Error — never catch it; if cleanup on any exit path is needed, use a detached Cancellation Scope rather than a broad catch; (2) CanceledFailure — rethrow it, optionally after cleanup in a detached Cancellation Scope; do not swallow it; (3) ActivityFailure, ChildWorkflowFailure, or ApplicationFailure that you recognize and can recover from — handle it; (4) Everything else — rethrow it. A plain RuntimeException that isn't recognized fails only the current Workflow Task, which retries indefinitely rather than failing the Workflow Execution. To fail the Workflow Execution deliberately, throw an ApplicationFailure.

Wrap checked exceptions instead of adding throws to method signatures

Activity and Workflow method signatures should not declare throws for checked exceptions. Instead, wrap a checked exception with Activity.wrap() inside an Activity, or Workflow.wrap() inside a Workflow, before rethrowing it. If e is a checked exception, wrap() returns a CheckedExceptionWrapper around it. The SDK unwraps it automatically while propagating the failure and attaches the original exception as the cause of the resulting ApplicationFailure. If e already extends RuntimeException, wrap() returns it unchanged. If e extends Error, wrap() rethrows it directly. Calling wrap() on an unchecked exception is a safe no-op.

Do not need to re-wrap an exception after unwrapping it

Once you have unwrapped a cause to inspect it, either rethrow the failure you caught or throw a new ApplicationFailure with the original exception set as its cause. There is no wrapper left to reapply — wrap() only matters at the point where a checked exception would otherwise need a throws declaration. Any unhandled exception an Activity or Workflow throws is already converted to an ApplicationFailure automatically when it crosses the Activity or Workflow boundary.

Exception failure cause chain layering

An exception thrown from an Activity or Child Workflow arrives at the caller wrapped with context about where it failed. A failure from an Activity called from a Child Workflow called from a parent Workflow looks like this by the time it reaches a synchronous client call: WorkflowFailedException (thrown to the client) → ChildWorkflowFailure (the child Workflow Execution failed) → ActivityFailure (the Activity Execution failed) → ApplicationFailure (what your code actually threw). Each wrapper adds context: ActivityFailure carries the Activity Type and Activity Id, ChildWorkflowFailure carries the Workflow Type and Workflow Id.

Read ApplicationFailure.getOriginalMessage() not getMessage()

When reading an ApplicationFailure, call getOriginalMessage() to get the exact text that was thrown, not getMessage(). getMessage() returns a decorated string such as message='Invalid credit card number', type='ValidationError', nonRetryable=true, which is meant for logs, not parsing. getOriginalMessage() returns the exact text you threw.

Match ApplicationFailure on getType() not instanceof original exception class

Match on ApplicationFailure.getType(), a stable String, not instanceof your original exception class. ApplicationFailure is final and the original exception object does not survive serialization: when an Activity in another process or another SDK language throws, the caller only ever gets an ApplicationFailure back, never your custom exception type. type defaults to the thrown exception's fully qualified class name unless you set it explicitly with ApplicationFailure.newFailure(message, type, ...).

Catch ActivityFailure not ApplicationFailure directly

Catch ActivityFailure (or ChildWorkflowFailure) around a call, not ApplicationFailure directly. The Activity or Child Workflow boundary always wraps the underlying failure. Always check for CanceledFailure as the cause before handling anything else, and rethrow it unhandled.

Cancellation is cooperative in Workflows

Cancellation is cooperative — the Worker never force-stops running Workflow code. A cancellation request cancels the current Cancellation Scope, and the next cancelable call inside it (an Activity, Timer, or Child Workflow) throws CanceledFailure. If cleanup is needed after a cancellation, for example to compensate an Activity that already applied its effect, run it in a detached Cancellation Scope, since a normal scope is a child of the one that was just canceled and any call inside it would be canceled immediately.

Use WorkerInterceptor to centralize Activity failure conversion

Activity code that calls several external services can use a WorkerInterceptor that overrides ActivityInboundCallsInterceptor.execute() to centralize failure mapping in one place instead of repeating catch blocks in every Activity implementation. Register it on the Worker Factory with WorkerFactoryOptions.newBuilder().setWorkerInterceptors(new ErrorNormalizingWorkerInterceptor()).build(). Because the interceptor sees the original exception before the SDK's default conversion runs, it is the layer to attach details and a non-retryable classification consistently.

Fail a Workflow Execution deliberately with ApplicationFailure

Throwing ApplicationFailure from Workflow code is the only way to fail a Workflow Execution deliberately. Any other unhandled exception fails only the current Workflow Task, which the Worker retries indefinitely. If you want specific plain exception types to fail the Workflow Execution instead of retrying the Workflow Task, list them with WorkflowImplementationOptions.setFailWorkflowExceptionTypes() when registering the Workflow implementation.

Never extend TemporalFailure in application code

Never extend TemporalFailure or any of its subclasses in application code — throw ApplicationFailure instead. The SDK reserves the other subclasses (ActivityFailure, ChildWorkflowFailure, CanceledFailure, TimeoutFailure, TerminatedFailure, ServerFailure) for its own use.

Example: Activity.wrap() with checked exception

static class GreetingActivitiesImpl implements GreetingActivities { @Override public String composeGreeting(String greeting, String name) { try { return callExternalService(greeting, name); // declares `throws IOException` } catch (IOException e) { throw Activity.wrap(e); } } } This example shows how to wrap a checked exception that would otherwise require a throws declaration.

Example: Catch and inspect ApplicationFailure cause

try { return activities.processCreditCard(orderId); } catch (ActivityFailure e) { if (e.getCause() instanceof ApplicationFailure appFailure) { if ("ValidationError".equals(appFailure.getType())) { return Result.rejected(appFailure.getOriginalMessage()); } } throw e; } This example shows how to catch ActivityFailure, check for CanceledFailure, inspect the cause chain, match on getType(), and read getOriginalMessage().

Example: Handle ActivityFailure and check for CanceledFailure

try { return activities.charge(order); } catch (ActivityFailure e) { if (e.getCause() instanceof CanceledFailure) { throw e; // never swallow cancellation } if (e.getCause() instanceof ApplicationFailure appFailure && "PaymentDeclined".equals(appFailure.getType())) { return Result.declined(appFailure.getOriginalMessage()); } throw e; // don't recognize it — propagate } This example shows the pattern for handling ActivityFailure with CanceledFailure check first, then specific ApplicationFailure handling.

Example: Cleanup in detached Cancellation Scope after CanceledFailure

try { activities.longRunningWork(); } catch (CanceledFailure e) { Workflow.newDetachedCancellationScope(() -> activities.compensate()).run(); throw e; // rethrow after cleanup so the Workflow Execution ends "Canceled" } This example shows how to run cleanup after a cancellation using a detached Cancellation Scope, then rethrow the CanceledFailure.

Example: Fail Workflow Execution deliberately with ApplicationFailure

if (order.getTotal().compareTo(BigDecimal.ZERO) <= 0) { throw ApplicationFailure.newNonRetryableFailure( "Order total must be positive: " + order.getTotal(), "InvalidOrderTotal"); } This example shows how to throw an ApplicationFailure to deliberately fail a Workflow Execution with a custom type and non-retryable classification.

Example: WorkerInterceptor for centralizing Activity failure conversion

public final class ErrorNormalizingWorkerInterceptor extends WorkerInterceptorBase { @Override public ActivityInboundCallsInterceptor interceptActivity(ActivityInboundCallsInterceptor next) { return new ActivityInboundCallsInterceptorBase(next) { @Override public ActivityOutput execute(ActivityInput input) { try { return super.execute(input); } catch (ApplicationFailure | TimeoutFailure | CanceledFailure f) { throw f; // already a well-formed Temporal failure — pass through } catch (PaymentDeclinedException e) { throw ApplicationFailure.newNonRetryableFailure(e.getMessage(), "PaymentDeclined", e.toDetail()); } catch (Exception e) { throw Activity.wrap(e); // uniform fallback: type = class name, retryable } } }; } } Register it on the Worker Factory: WorkerFactoryOptions.newBuilder() .setWorkerInterceptors(new ErrorNormalizingWorkerInterceptor()) .build(); This example shows how to centralize domain exception to ApplicationFailure conversion in a WorkerInterceptor.

When catching ActivityFailure to handle retries, avoid catching generic Exception

When catching ActivityFailure in workflow code to handle retries, specifically catch ActivityFailure (or ChildWorkflowFailure) around a call, not ApplicationFailure directly or a generic Exception. A generic Exception catch can swallow important control signals like CanceledFailure and other typed Temporal failures that need special handling. Always check for CanceledFailure as the cause of ActivityFailure before handling anything else.

Give your agent this brain