Pick a theme:

Aborting async JavaScript

Mini-Tutorial about how to abort asynchronous JavaScript

by Lea Rosema

This is a follow-up post about something I wrote on dev.to, 5 years ago: Aborting a fetch request.

As I mentioned, aborting is not too intuitive. When using fetch, you can provide a signal in the request options, which can then be used to abort operations.

This can not only be used inside the fetch API but inside everything asynchronous.

Wait, Vanilla JavaScript has signals?

anchor

Yes. But not the Angular kind. It's solely for aborting async operations and that's why it's called AbortSignal.

It works like this: you create a new AbortController.

The controller exposes an abort method and a signal property. The signal can be used to listen on abort events, via signal.addEventListener.

Code Example

anchor
let abortController = null;
let counter = 1;

async function loop(t = 0, signal) {
  const timeout = setTimeout((t) => loop(t, signal), 1000);

  $counter.innerHTML = counter;
  counter++;
  signal.addEventListener("abort", () => {
    clearTimeout(timeout);
    $counter.innerHTML = "aborted :)";
  });
}

abortButton.addEventListener("click", () => {
  if (! abortController) return;
  abortController.abort();
  abortController = null;
});

runButton.addEventListener("click", () => {
  
  if (abortController) return;
  abortController = new AbortController();
  counter = 1;
  
  // The abort signal is a property of 
  // the AbortController, which we pass to the 
  // async loop function. 
  // We could even "await" it here, but as we
  // don't do anything further afterwards, it can be a 
  // fire and forget.
  const abortSignal = abortController.signal;
  loop(0, abortSignal);
});

Nested asynchronous functions

anchor

When you have nested asynchronous functions, don't forget to pass the signal down.

function wait(ms, signal) {
  return new Promise((resolve, reject) => {
    const timerId = window.setTimeout(resolve, ms)
    signal?.addEventListener('abort', () => {
      window.clearTimeout(timerId)
      reject()
    })
  })
}

async function incrementTripleTimes(signal) {
  $counter2.innerHTML = counter2;
  await wait(1000, signal)
  $counter2.innerHTML = ++counter2;
  await wait(1000, signal)
  $counter2.innerHTML = ++counter2;
  await wait(1000, signal)
  $counter2.innerHTML = ++counter2;
}

async function loop2(t = 0, signal) {
  // uncomment below and see what happens
  // signal = null
  signal?.addEventListener("abort", () => {
    $counter2.innerHTML = "aborted :)";
  });

  await incrementTripleTimes(signal);
  await incrementTripleTimes(signal);
  await incrementTripleTimes(signal);
  
  console.log('restarting loop')
  window.setTimeout((t) => loop2(t, signal), 0);
}

runButton2.addEventListener("click", () => {
  if (abortController2) return;
  abortController2 = new AbortController();
  counter2 = 1;
  loop2(0, abortController2.signal);
})

abortButton2.addEventListener("click", () => {
  if (! abortController2) return;
  abortController2.abort();
  abortController2 = null;
});

Check the full demo on CodePen.

Sources

anchor