Failure detection - TypeScript SDK feature guide
This page shows how to do the following:
- Raise and Handle Exceptions
- Deliberately Fail Workflows
- Workflow Timeouts
- Workflow retries
- Activity Timeouts
- Activity Retry Policy
- Activity next Retry delay
- Heartbeat an Activity
- Activity Heartbeat Timeout
Raise and Handle Exceptions
In each Temporal SDK, error handling is implemented idiomatically, following the conventions of the language.
Temporal uses several different error classes internally — for example, CancelledFailure
in the Typescript SDK, to handle a Workflow cancellation.
You should not raise or otherwise implement these manually, as they are tied to Temporal platform logic.
The one Temporal error class that you will typically raise deliberately is ApplicationFailure
.
In fact, any other exceptions that are raised from your Typescript code in a Temporal Activity will be converted to an ApplicationError
internally.
This way, an error's type, severity, and any additional details can be sent to the Temporal Service, indexed by the Web UI, and even serialized across language boundaries.
In other words, these two code samples do the same thing:
class InvalidChargeError extends Error {
constructor(message: string) {
super(message);
this.name = "InvalidChargeError";
Object.setPrototypeOf(this, CustomError.prototype);
}
}
if (chargeAmount < 0) {
throw new InvalidChargeError(`Invalid charge amount: ${chargeAmount} (must be above zero)`);
}
if (chargeAmount < 0) {
throw ApplicationFailure.create({
message: `Invalid charge amount: ${chargeAmount} (must be above zero)`,
type: 'InvalidChargeError',
});
}
Depending on your implementation, you may decide to use either method.
One reason to use the Temporal ApplicationFailure
class is because it allows you to set an additional non_retryable
parameter.
This way, you can decide whether an error should not be retried automatically by Temporal.
This can be useful for deliberately failing a Workflow due to bad input data, rather than waiting for a timeout to elapse:
if (chargeAmount < 0) {
throw ApplicationFailure.create({
message: `Invalid charge amount: ${chargeAmount} (must be above zero)`,
nonRetryable: true
});
}
You can alternately specify a list of errors that are non-retryable in your Activity Retry Policy.
Failing Workflows
One of the core design principles of Temporal is that an Activity Failure will never directly cause a Workflow Failure — a Workflow should never return as Failed unless deliberately.
The default retry policy associated with Temporal Activities is to retry them until reaching a certain timeout threshold.
Activities will not actually return a failure to your Workflow until this condition or another non-retryable condition is met.
At this point, you can decide how to handle an error returned by your Activity the way you would in any other program.
For example, you could implement a Saga Pattern that uses try
and catch
blocks to "unwind" some of the steps your Workflow has performed up to the point of Activity Failure.
You will only fail a Workflow by manually raising an ApplicationFailure
from the Workflow code.
You could do this in response to an Activity Failure, if the failure of that Activity means that your Workflow should not continue:
try {
await addAddress();
} catch (err) {
if (err instanceof ActivityFailure && err.cause instanceof ApplicationFailure) {
log.error(err.cause.message);
throw err;
}
}
This works differently in a Workflow than raising exceptions from Activities.
In an Activity, any Typescript exceptions or custom exceptions are converted to a Temporal ApplicationFailure
.
In a Workflow, any exceptions that are raised other than an explicit Temporal ApplicationFailure
will only fail that particular Workflow Task and be retried.
This includes any typical Typescript runtime errors like an undefined
error that are raised automatically.
These errors are treated as bugs that can be corrected with a fixed deployment, rather than a reason for a Temporal Workflow Execution to return unexpectedly.
Workflow Timeouts
How to set Workflow Timeouts using the Temporal TypeScript SDK
Each Workflow timeout controls the maximum duration of a different aspect of a Workflow Execution.
Before we continue, we want to note that we generally do not recommend setting Workflow Timeouts, because Workflows are designed to be long-running and resilient. Instead, setting a Timeout can limit its ability to handle unexpected delays or long-running processes. If you need to perform an action inside your Workflow after a specific period of time, we recommend using a Timer.
Workflow Timeouts are set when starting a Workflow using either the Client or Workflow API.
- Workflow Execution Timeout - restricts the maximum amount of time that a single Workflow Execution can be executed
- Workflow Run Timeout: restricts the maximum amount of time that a single Workflow Run can last
- Workflow Task Timeout: restricts the maximum amount of time that a Worker can execute a Workflow Task
The following properties can be set on the WorkflowOptions
when starting a Workflow using either the Client or Workflow API:
await client.workflow.start(example, {
taskQueue,
workflowId,
// Set Workflow Timeout duration
workflowExecutionTimeout: '1 day',
// workflowRunTimeout: '1 minute',
// workflowTaskTimeout: '30 seconds',
});
Workflow retries
How to set Workflow retries using the Temporal TypeScript SDK
A Retry Policy can work in cooperation with the timeouts to provide fine controls to optimize the execution experience.
Use a Retry Policy to retry a Workflow Execution in the event of a failure.
Workflow Executions do not retry by default, and Retry Policies should be used with Workflow Executions only in certain situations.
The Retry Policy can be set through the WorkflowOptions.retry
property when starting a Workflow using either the Client or Workflow API.
const handle = await client.workflow.start(example, {
taskQueue,
workflowId,
retry: {
maximumAttempts: 3,
maximumInterval: '30 seconds',
},
});
Activity Timeouts
How to set Activity Timeouts using the Temporal TypeScript SDK
Each Activity Timeout controls the maximum duration of a different aspect of an Activity Execution.
The following Timeouts are available in the Activity Options:
- Schedule-To-Close Timeout: is the maximum amount of time allowed for the entire Activity Execution, from when the Activity Task is initially scheduled by the Workflow to when the server receives a successful completion for that Activity Task
- Start-To-Close Timeout: is the maximum time allowed for a single Activity Task Execution, from when the Activity Task Execution gets polled by a Worker to when the server receives a successful completion for that Activity Task
- Schedule-To-Start Timeout: is the maximum amount of time that is allowed from when an Activity Task is initially scheduled by the Workflow to when a Worker polls the Activity Task Execution
An Activity Execution must have either the Start-To-Close or the Schedule-To-Close Timeout set.
The following properties can be set on the ActivityOptions
when creating Activity proxy functions using the proxyActivities()
API:
const { myActivity } = proxyActivities<typeof activities>({
scheduleToCloseTimeout: '5m',
// startToCloseTimeout: "30s", // recommended
// scheduleToStartTimeout: "60s",
});
Activity Retry Policy
How to set an Activity Retry Policy using the Temporal TypeScript SDK
A Retry Policy works in cooperation with the timeouts to provide fine controls to optimize the execution experience.
Activity Executions are automatically associated with a default Retry Policy if a custom one is not provided.
To set an Activity's Retry Policy in TypeScript, assign the ActivityOptions.retry
property when creating the corresponding Activity proxy function using the proxyActivities()
API.
const { myActivity } = proxyActivities<typeof activities>({
// ...
retry: {
initialInterval: '10s',
maximumAttempts: 5,
},
});
Activity next Retry delay
How to override the next Retry delay following an Activity failure using the Temporal TypeScript SDK
The time to wait after a retryable Activity failure until the next retry is attempted is normally determined by that Activity's Retry Policy.
However, an Activity may override that duration when explicitly failing with an ApplicationFailure
by setting a next Retry delay.
To override the next Retry delay for an ApplicationFailure
thrown by an Activity in TypeScript, provide the nextRetryDelay
property on the object argument of the ApplicationFailure.create()
factory method.
throw ApplicationFailure.create({
// ...
nextRetryDelay: '15s',
});
Heartbeat an Activity
How to Heartbeat an Activity using the Temporal TypeScript SDK
An Activity Heartbeat is a ping from the Worker Process that is executing the Activity to the Temporal Service. Each Heartbeat informs the Temporal Service that the Activity Execution is making progress and the Worker has not crashed. If the Temporal Service does not receive a Heartbeat within a Heartbeat Timeout time period, the Activity will be considered as timed out and another Activity Task Execution may be scheduled according to the Retry Policy.
Activity Cancellations are delivered to Activities from the Temporal Service when they Heartbeat. Activities that don't Heartbeat can't get notified of Cancellation requests.
Heartbeats may not always be sent to the Temporal Service—they may be throttled by the Worker. Heartbeat throttling may lead to Cancellation getting delivered later than expected.
To Heartbeat an Activity Execution in TypeScript, call the heartbeat()
function from the Activity implementation.
export async function myActivity(): Promise<void> {
for (let progress = 1; progress <= 1000; ++progress) {
// Do something that takes time
await sleep('1s');
heartbeat();
}
}
An Activity may optionally checkpoint its progression, by providing a details
argument to the heartbeat()
function.
Should the Activity Execution times out and gets retried, then the Temporal Server will provide the details
from the last Heartbeat it received to the next Activity Execution.
This can be used to allow the Activity to efficiently resume its work.
export async function myActivity(): Promise<void> {
// Resume work from latest heartbeat, if there's one, or start from 1 otherwise
const startingPoint = activityInfo().heartbeatDetails?.progress ?? 1;
for (let progress = startingPoint; progress <= 1000; ++progress) {
// Do something that takes time
await sleep('1s');
heartbeat({ progress });
}
}
Activity Heartbeat Timeout
How to set a Heartbeat Timeout using the Temporal TypeScript SDK
A Heartbeat Timeout works in conjunction with Activity Heartbeats. If the Temporal Server doesn't receive a Heartbeat before expiration of the Heartbeat Timeout, the Activity is considered as timed out and another Activity Task Execution may be scheduled according to the Retry Policy.
To set an Activity's Heartbeat Timeout in TypeScript, set the ActivityOptions.heartbeatTimeout
property when creating the corresponding Activity proxy functions using the proxyActivities()
API.
const { myLongRunningActivity } = proxyActivities<typeof activities>({
// ...
heartbeatTimeout: '30s',
});