Skip to main content

Workflow scopes and Cancellation

In the Node SDK, Workflows are represented internally by a tree of scopes. The main function runs in the root scope. Cancellation propagates from outer scopes to inner ones and is handled by catching CancelledErrors thrown by cancellable operations (see below).

Scopes are created using the CancellationScope constructor or the static helper methods cancellable, nonCancellable, and withTimeout.

When a CancellationScope is cancelled, it propagates cancellation in any child scopes and of any cancellable operations created within it, such as:

Examples#

Cancel a timer from Workflow code#

packages/test/src/workflows/cancel-timer-immediately.ts

import { CancelledError, CancellationScope, sleep } from '@temporalio/workflow';
import { Empty } from '../interfaces';
async function main(): Promise<void> {
// Timers and Activities are automatically cancelled when their containing scope is cancelled.
try {
await CancellationScope.cancellable(async () => {
const promise = sleep(1); // <-- Will be cancelled because it is attached to this closure's scope
CancellationScope.current().cancel();
await promise; // <-- Promise must be awaited in order for `cancellable` to throw
});
} catch (e) {
if (e instanceof CancelledError) {
console.log('Timer cancelled ๐Ÿ‘');
} else {
throw e; // <-- Fail the workflow
}
}
}
export const workflow: Empty = { main };

Alternatively, the preceding can be written as#

packages/test/src/workflows/cancel-timer-immediately-alternative-impl.ts

import { CancelledError, CancellationScope, sleep } from '@temporalio/workflow';
export async function main(): Promise<void> {
try {
const scope = new CancellationScope();
const promise = scope.run(() => sleep(1));
scope.cancel(); // <-- Cancel the timer created in scope
await promise; // <-- Throws CancelledError
} catch (e) {
if (e instanceof CancelledError) {
console.log('Timer cancelled ๐Ÿ‘');
} else {
throw e; // <-- Fail the workflow
}
}
}

Run multiple activities with a single deadline#

packages/test/src/workflows/multiple-activities-single-timeout.ts

import { CancellationScope } from '@temporalio/workflow';
import { httpGetJSON } from '@activities';
export async function main(urls: string[], timeoutMs: number): Promise<any[]> {
// If timeout triggers before all activities complete
// the Workflow will fail with a CancelledError.
return CancellationScope.withTimeout(timeoutMs, () => Promise.all(urls.map((url) => httpGetJSON(url))));
}

nonCancellable prevents cancellation from propagating to children#

packages/test/src/workflows/non-cancellable-shields-children.ts

import { CancellationScope } from '@temporalio/workflow';
import { httpGetJSON } from '@activities';
export async function main(url: string): Promise<any> {
// Prevent Activity from being cancelled and await completion.
// Note that the Workflow is completely oblivious and impervious to cancellation in this example.
return CancellationScope.nonCancellable(() => httpGetJSON(url));
}

cancelRequested may be awaited upon to make Workflow aware of cancellation while waiting on nonCancellable scopes#

packages/test/src/workflows/cancel-requested-with-non-cancellable.ts

import { CancelledError, CancellationScope } from '@temporalio/workflow';
import { httpGetJSON } from '@activities';
export async function main(url: string): Promise<any> {
let result: any = undefined;
const scope = new CancellationScope({ cancellable: false });
const promise = scope.run(() => httpGetJSON(url));
try {
result = await Promise.race([scope.cancelRequested, promise]);
} catch (err) {
if (!(err instanceof CancelledError)) {
throw err;
}
// Prevent Workflow from completing so Activity can complete
result = await promise;
}
return result;
}

Handle Workflow cancellation by an external client while an Activity is running#

packages/test/src/workflows/handle-external-workflow-cancellation-while-activity-running.ts

import { CancelledError, CancellationScope } from '@temporalio/workflow';
import { httpPostJSON, cleanup } from '@activities';
export async function main(url: string, data: any): Promise<void> {
try {
await httpPostJSON(url, data);
} catch (err) {
if (err instanceof CancelledError) {
console.log('Workflow cancelled');
// Cleanup logic must be in a nonCancellable scope
// If we'd run cleanup outside of a nonCancellable scope it would've been cancelled
// before being started because the Workflow's root scope is cancelled.
await CancellationScope.nonCancellable(() => cleanup(url));
}
throw err; // <-- Fail the Workflow
}
}

Complex flows may be achieved by nesting cancellation scopes#

packages/test/src/workflows/nested-cancellation.ts

import { CancelledError, CancellationScope } from '@temporalio/workflow';
import { setup, httpPostJSON, cleanup } from '@activities';
export async function main(url: string): Promise<void> {
await CancellationScope.cancellable(async () => {
await CancellationScope.nonCancellable(() => setup());
try {
await CancellationScope.withTimeout(1000, () => httpPostJSON(url, { some: 'data' }));
} catch (err) {
if (err instanceof CancelledError) {
await CancellationScope.nonCancellable(() => cleanup(url));
}
throw err;
}
});
}

Sharing promises between scopes#

Operations like timers and Activites are cancelled by the cancellation scope they were created in. Promises returned by these operations can be awaited in different scopes.

packages/test/src/workflows/shared-promise-scopes.ts

import { CancellationScope } from '@temporalio/workflow';
import { httpGetJSON } from '@activities';
export async function main(): Promise<any> {
// Start activities in the root scope
const p1 = httpGetJSON('http://url1.ninja');
const p2 = httpGetJSON('http://url2.ninja');
const scopePromise = CancellationScope.cancellable(async () => {
const first = await Promise.race([p1, p2]);
// Does not cancel activity1 or activity2 as they're linked to the root scope
CancellationScope.current().cancel();
return first;
});
return await scopePromise;
// The Activity that did not complete will effectivly be cancelled when
// Workflow completes unless explicitly awaited upon:
// await Promise.all([p1, p2]);
}

packages/test/src/workflows/shield-awaited-in-root-scope.ts

import { CancellationScope } from '@temporalio/workflow';
import { httpGetJSON } from '@activities';
export async function main(): Promise<any> {
let p: Promise<any> | undefined = undefined;
await CancellationScope.nonCancellable(async () => {
p = httpGetJSON('http://example.com'); // <-- Start activity in nonCancellable scope without awaiting completion
});
// Activity is shielded from cancellation even though it is awaited in the cancellable root scope
return p;
}

Callbacks and cancellation scopes#

Callbacks are not particularly useful in Workflows because all meaningful asynchronous operations return Promises.

In the odd case that user code utilizes callbacks, CancellationScope.cancelRequested can be used to subscribe to cancellation.

packages/test/src/workflows/cancellation-scopes-with-callbacks.ts

import { CancellationScope } from '@temporalio/workflow';
function doSomehing(callback: () => any) {
setTimeout(callback, 10);
}
export async function main(): Promise<void> {
await new Promise<void>((resolve, reject) => {
CancellationScope.cancellable(async () => {
doSomehing(resolve);
CancellationScope.current().cancelRequested.catch(reject);
});
});
}

Get notified of updates