Frontend Development 10 min read

Implementing Cancellation and Progress Notification for JavaScript Promises

This article examines advanced JavaScript Promise techniques by presenting practical implementations for cancelling a pending promise and for notifying progress during asynchronous operations, complete with code examples, explanations of underlying concepts, and discussion of their relevance in interviews and real‑world development.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Cancellation and Progress Notification for JavaScript Promises

The article explores advanced usage of JavaScript Promise, focusing on two rarely discussed features: cancellation and progress notification.

It first explains why a Promise cannot simply stay in the pending state and why cancellation requires external control, then demonstrates a simple timeout‑based cancellation example.

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
      // handler data, no resolve and reject
    }, 1000);
});
console.log(p); // Promise{
} 💡

Next, it shows a more complete solution using Promise.race to simulate a request that can timeout, illustrating that race does not actually abort the underlying network call.

const getData = () =>
  new Promise((resolve) => {
    setTimeout(() => {
      console.log("发送网络请求获取数据"); // ❗
      resolve("success get Data");
    }, 2500);
  });

const timer = () =>
  new Promise((_, reject) => {
    setTimeout(() => {
      reject("timeout");
    }, 2000);
  });

const p = Promise.race([getData(), timer()])
  .then((res) => {
    console.log("获取数据:", res);
  })
  .catch((err) => {
    console.log("超时: ", err);
  });

The author then presents a custom cancellation mechanism using a CancelToken class, two buttons for sending and cancelling, and a promise that resolves when the cancel button is clicked, clearing the pending timeout.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button id="send">Send</button>
    <button id="cancel">Cancel</button>
    <script>
      class CancelToken {
        constructor(cancelFn) {
          this.promise = new Promise((resolve, reject) => {
            cancelFn(() => {
              console.log("delay cancelled");
              resolve();
            });
          });
        }
      }
      const sendButton = document.querySelector("#send");
      const cancelButton = document.querySelector("#cancel");

      function cancellableDelayedResolve(delay) {
        console.log("prepare send request");
        return new Promise((resolve, reject) => {
          const id = setTimeout(() => {
            console.log("ajax get data");
            resolve();
          }, delay);

          const cancelToken = new CancelToken((cancelCallback) =>
            cancelButton.addEventListener("click", cancelCallback)
          );
          cancelToken.promise.then(() => clearTimeout(id));
        });
      }
      sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
    </script>
  </body>
</html>

Building on that idea, a CancelPromise class is introduced that encapsulates a delay, a request function, and explicit cancel logic, allowing the developer to start a delayed request and abort it before the timeout.

class CancelPromise {
  // delay: cancel deadline, request: function returning a promise
  constructor(delay, request) {
    this.req = request;
    this.delay = delay;
    this.timer = null;
  }

  delayResolve() {
    return new Promise((resolve, reject) => {
      console.log("prepare request");
      this.timer = setTimeout(() => {
        console.log("send request");
        this.timer = null;
        this.req().then(resolve, reject);
      }, this.delay);
    });
  }

  cancelResolve() {
    console.log("cancel promise");
    this.timer && clearTimeout(this.timer);
  }
}

function getData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("this is data");
    }, 2000);
  });
}

const cp = new CancelPromise(1000, getData);
sendButton.addEventListener("click", () =>
  cp.delayResolve().then((res) => console.log("拿到数据:", res))
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

For progress notification, the article introduces a TrackablePromise class that extends Promise and adds a notify method, enabling observers to receive intermediate status updates during asynchronous execution.

class TrackablePromise extends Promise {
  constructor(executor) {
    const notifyHandlers = [];
    super((resolve, reject) => {
      return executor(resolve, reject, (status) => {
        notifyHandlers.map((handler) => handler(status));
      });
    });
    this.notifyHandlers = notifyHandlers;
  }
  notify(notifyHandler) {
    this.notifyHandlers.push(notifyHandler);
    return this;
  }
}
let p = new TrackablePromise((resolve, reject, notify) => {
  function countdown(x) {
    if (x > 0) {
      notify(`${20 * x}% remaining`);
      setTimeout(() => countdown(x - 1), 1000);
    } else {
      resolve();
    }
  }
  countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));

A more realistic async/await example combines the TrackablePromise with multiple data‑fetch steps, calling notify after each stage and finally resolving with all results.

function getData(timer, value) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(value);
    }, timer);
  });
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
  try {
    const res1 = await getData1();
    notify("已获取到一阶段数据");
    const res2 = await getData2();
    notify("已获取到二阶段数据");
    const res3 = await getData3();
    notify("已获取到三阶段数据");
    resolve([res1, res2, res3]);
  } catch (error) {
    notify("出错!");
    reject(error);
  }
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));

The author concludes that although native Promise cancellation was proposed and later withdrawn, implementing a custom cancel token remains an interesting interview exercise, and progress notifications, while optional, can be valuable in complex asynchronous workflows.

frontendJavaScriptPromiseCancellationProgress
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.