Modern web applications often perform asynchronous operations such as network requests, user-driven workflows, or long-running computations. In many cases, these operations may need to be cancelled before completion to improve performance, reduce unnecessary server load, and provide a smoother, more responsive user experience. The AbortController and AbortSignal APIs provide a standard way to handle such cancellation.
The AbortController pattern
The AbortController API is the standard way to signal cancellation in JavaScript. It creates an associated AbortSignal, which serves as a communication channel between the controller and any asynchronous operation. When abort() is called on the controller, the signal's aborted property becomes true
. This lets the function detect cancellation and respond by stopping execution, performing cleanup, or throwing an Abort
.
const controller = new AbortController();
const signal = controller.signal;
// Pass `signal` to an async operation:
someAsyncFunction({ signal });
// cancel the operation
controller.abort();
Using AbortController with the ArcGIS Maps SDK for JavaScript
Many methods in ArcGIS Maps SDK for JavaScript (@arcgis/core
) support this standard by accepting an Abort
. This enables robust, responsive cancellation of tasks that might otherwise be left running unnecessarily. Cancellation is particularly valuable in high-frequency or interruptible workflows, such as:
- Querying features as the mouse moves over a map
- Animating view transitions as the user pans or zooms
- Adjusting a slice plane in a 3D scene
An Abort
can be passed as part of the options
object to methods like FeatureLayer.queryFeatures, MapView.goTo, reactiveUtils.whenOnce, among other methods. When the signal is aborted, the operation terminates early, helping conserve resources and ensuring your application remains responsive.
Always check the official core API documentation for the specific method you're using to confirm support for Abort
.
Example: Using Abort Signal
with reactive Utils.when Once()
reactiveUtils.whenOnce() returns a promise that resolves the first time a specified reactive condition becomes true
. It supports cancellation via an Abort
, which allows you to stop waiting if the condition is no longer relevant. If aborted, the promise rejects with an Abort
.
// Use an AbortSignal to cancel waiting for the view's animation
const abortController = new AbortController();
// Wait until view.animation exists (i.e., becomes truthy) or cancel if aborted
reactiveUtils.whenOnce(
() => view?.animation,
{ signal: abortController.signal }
)
.then((animation) => {
// This runs only if the animation becomes truthy before aborting
console.log(`View animation state is ${animation.state}`);
})
.catch((error) => {
if (error.name === "AbortError") {
// This runs if the wait was cancelled before the condition became true
console.log("Animation observation cancelled");
} else {
throw error;
}
});
// Stop waiting for the condition to be truthy
const cancelObservation = () => {
abortController.abort();
};
Cancelling the when
promise is useful in scenarios where the condition may no longer matter, for example, if the user navigates away or another condition becomes more relevant. It helps avoid executing stale or unnecessary logic that could confuse users or waste resources.
Error handling and AbortError
When an operation is cancelled, most APIs (including the ArcGIS SDK) reject the returned promise with an Abort
. It is good practice to check for Abort
in your error handling logic. This specific error is thrown when a signal is aborted and indicates intentional cancellation, not a failure of the operation. Catching it explicitly allows your app to differentiate between user-driven cancellation and unexpected failures—preventing misleading error messages or unnecessary retries.
try {
const results = await layerView.queryFeatures(
{
where: "someField = 'someValue'",
returnGeometry: true
},
{ signal: controller.signal }
);
console.log("Query completed:", results.features);
} catch (error) {
if (error.name === "AbortError") {
console.log("Query operation was cancelled.");
return;
}
throw error; // Handle other errors as needed.
}
Managing repeated operations and avoiding race conditions
When user input can trigger operations repeatedly, such as clicks or mouse moves, or slider changes, it is important to cancel any ongoing operation before starting a new one. Failing to do so can lead to
- Race conditions: where older operations overwrite newer results
- Memory leaks: if references to old
Abort
s lingerController
In this case, use a pattern that tracks the current operation’s Abort
and aborts it before launching a new one. Always check if the operation’s signal
is still current before processing results, and clean up references properly to avoid leaks.
The queryFeatures() method queries on feature layers or their layer views and supports cancellation via an AbortSignal passed as the second options parameter. This lets you cancel ongoing queries when input changes, such as from a calcite slider. Aborting is important because rapid input changes can trigger multiple queries; without cancellation, outdated queries waste resources and may overwrite newer results. Cancelling previous queries avoids outdated results and ensures only the latest input is processed.
let currentController;
const slider = document.querySelector("calcite-slider");
const layerView = await viewElement.whenLayerView(layer);
slider.addEventListener("calciteSliderInput", async () => {
// Cancel any ongoing query
currentController?.abort();
// Create a new controller for the current query
currentController = new AbortController();
const { signal } = currentController;
const sliderValue = slider.value;
try {
// Query features with a filter based on the slider's value
const results = await layerView.queryFeatures(
{
where: `someField = ${sliderValue}`,
returnGeometry: true
},
{ signal }
);
if (signal.aborted) {
// Query was cancelled, ignore results
return;
}
// Process the results — for example, update the map or
// a chart with the new features
console.log("Query results:", results.features);
} catch (error) {
if (error.name === "AbortError") {
console.log("Query cancelled due to slider input change.");
} else {
throw error;
}
} finally {
// Clear the controller if it's still the current one
if (currentController?.signal === signal) {
currentController = null;
}
}
});
This pattern ensures only the most relevant operation completes, avoids memory leaks, and helps maintain UI consistency.
Looping and repeated operations
For repeated or continuous workflows (e.g., placing multiple objects until cancelled), use a loop with cancellation support. This pattern lets you start a cancellable async operation multiple times, waiting for each placement to complete before starting the next—while allowing the user to cancel the entire sequence at any time. The key is to check the Abort
regularly and handle cancellation gracefully. The loop continues calling the async operation until the signal is aborted or you decide to stop.
async function startLoopedPlacement(): Promise<void> {
currentController?.abort();
currentController = new AbortController();
const signal = currentController.signal;
try {
while (true) {
// Continue placing until cancelled or explicitly stopped
await analysisView.place({ signal });
// Check if the user cancelled during the placement
if (signal.aborted) {
console.log("Placement operation was cancelled.");
break; // Exit the loop on cancellation
}
// Add logic to exit the loop if needed.
}
} catch (error) {
// ...handle errors...
} finally {
if (currentController?.signal === signal) {
currentController = null;
}
}
}
Helpful utilities in @arcgis/core
The promiseUtils module provides a collection of helpful utilities designed to simplify working with promises, especially when handling asynchronous operations that support cancellation. These helpers make it easier to manage common scenarios such as detecting cancellation errors, debouncing function calls to avoid redundant executions, and controlling the lifecycle of promise-based workflows in a clean and efficient way.
The promiseUtils.isAbortError
The isAbortError() function helps you quickly check if an error is caused by an operation being cancelled with an AbortError. This makes it easy to handle cancellations separately from real errors, allowing your app to respond appropriately without confusing the two.
import { isAbortError } from "@arcgis/core/core/promiseUtils.js";
try {
await someAsyncOperation({ signal: controller.signal });
} catch (error) {
if (isAbortError(error)) {
console.log("Operation was cancelled.");
} else {
throw error;
}
}
The promiseUtils.debounce
The promiseUtils.debounce() ensures an async function is not invoked more than once at a time. If the function is called again before a previous call has completed, the previous call is automatically cancelled using Abort
. This is especially useful in highly interactive applications, such as those that:
- Query statistics on mouse move
- Respond to drag gestures
- Recalculate results on rapid input changes such as dragging slider thumbs
By cancelling prior calls, debounce()
reduces unnecessary work and improves performance and responsiveness. To see debounce()
in action, explore samples that use this method.
Custom Utility Functions
While not built into @arcgis/core
, the following utilities can simplify error-handling patterns:
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
class AbortError extends Error {
constructor(message = "The operation was aborted") {
super(message);
this.name = "AbortError";
}
}
// Throws immediately if the signal is already aborted.
function throwAbortErrorIfAborted(signal: AbortSignal): void {
if (signal.aborted) {
throw new AbortError();
}
}
// Rethrows only if the error is an AbortError.
function throwIfAbortError(error: unknown): void {
if (promiseUtils.isAbortError(error)) {
throw error;
}
}
// Rethrows only if the error is NOT an AbortError.
function throwIfNotAbortError(error: unknown): void {
if (!promiseUtils.isAbortError(error)) {
throw error;
}
}
// Usage example
try {
await someAsyncOperation({ signal: controller.signal });
} catch (error) {
// Replaces previous logic: catch abort errors, rethrow others.
throwIfNotAbortError(error);
}
Combining multiple AbortSignals
Sometimes, an operation may depend on multiple sources of cancellation, such as user actions or timeout timers. In these cases, you want the operation to cancel as soon as any of the related signals is aborted. This pattern lets your async operation respond to multiple cancellation triggers, such as user actions or timeouts. To cancel an operation when any of several signals is aborted, use AbortSignal.any(). This method returns a new AbortSignal that becomes aborted as soon as any of the input signals is aborted.
This is useful for coordinating cancellation logic across different parts of your app without manually listening to multiple signals.
const userAbortController = new AbortController();
const timeoutAbortController = new AbortController();
// Automatically abort after 5 seconds
setTimeout(() => timeoutAbortController.abort(), 5000);
// Create a combined signal that aborts if either user cancels or timeout triggers
const combinedSignal = AbortSignal.any([
userAbortController.signal,
timeoutAbortController.signal,
]);
try {
await someAsyncOperation({ signal: combinedSignal });
console.log("Operation completed successfully.");
} catch (error) {
if (error.name === "AbortError") {
console.log("Operation cancelled by user or timed out.");
} else {
throw error;
}
}
// Example: Cancel operation manually (e.g., user clicks cancel button)
function cancelOperation() {
userAbortController.abort();
}
This pattern allows your async operation to respond to multiple cancellation triggers gracefully, making your app more responsive and robust.
Best practices and common pitfalls
1. Aborting does not instantly stop operations
Calling abort()
on an Abort
does not immediately stop an ongoing operation. It simply signals cancellation by setting the associated Abort
flag to true
. The actual termination depends on the operation’s implementation. Some may ignore the signal or take time to respond.
To ensure cancellation works as expected, use APIs that support Abort
, or handle the signal manually in your own async code:
async function longOperation({ signal }) {
for (let i = 0; i < 10000; i++) {
if (signal.aborted) {
throw new AbortError();
}
await doWork(i); // Simulated long-running task
}
}
2. Avoid side effects if the signal was aborted
Even if an async operation completes, cancellation may have occurred during the await
. Always check signal.aborted
before performing side effects like updating UI, processing results, or triggering follow-up tasks. This avoids applying stale or irrelevant data after the user has moved on.
await doSomethingAsync({ signal });
if (signal.aborted) {
// Skip side effects — the result is no longer valid
return;
}
// Only runs if the operation wasn’t cancelled
performSideEffect();
Using Abort
and Abort
effectively enables your ArcGIS JavaScript applications to handle cancellation robustly. Implementing these patterns reduces wasted resources, avoids race conditions, and improves user experience. Explore these samples to see Abort
in action.