What is the best way to cancel a Promise if it hasn't been resolved yet

Let's consider a situation where I have implemented a search function to make an HTTP call. Each call made can have varying durations, and it is crucial for the system to cancel any previous HTTP requests and only await results from the latest call.

async function search(timeout){

   const data = await simulateHTTPRequest(timeout)
   return data;

}
// The simulateHTTPRequest function is used purely for representational purposes
function simulatedHTTPRequest(timeout){
   return new Promise(resolve,reject){
       setTimeout(function(){      
           resolve()
       },timeout) 
   }
}
search(200)
.then(function(){console.log('search1 resolved')})
.catch(function() {console.log('search1 rejected')})
search(2000)
.then(function(){console.log('search2 resolved')})
.catch(function(){console.log('search2 rejected')})
search(1000)
.then(function(){console.log('search3 resolved')})
.catch(function(){console.log('search3 rejected')})

The desired output needs to display "search1 resolved", "search2 rejected", and "search3 resolved".

How can this particular scenario be achieved?

Answer №1

While promises cannot be directly canceled, they can be terminated by rejecting them in a limited sense.

To achieve cancellation, you can use Promise.race() in combination with a special function that allows for cancellations.

function makeCancellable(fn) {
    var reject_; // store for the latest 'reject' function
    return function(...params) {
        if(reject_) reject_(new Error('_cancelled_')); // If there is a previous 'reject', cancel it.
                                                       // This only works if the previous race is still ongoing.
        let canceller = new Promise((resolve, reject) => { // create a cancellation promise
            reject_ = reject; // store the canceller's 'reject' function
        });
        return Promise.race([canceller, fn.apply(null, params)]); // race between the main promise and the canceller
    }
}

If your HTTP call function is named httpRequest (avoid using confusing names like 'promise'), you can make it cancellable as follows:

const search = makeCancellable(httpRequest);

Each time you call search(), the cached 'reject' function is invoked to effectively "cancel" the previous search (assuming it has not yet been resolved).

// Search 1: No need for cancellation - httpRequest(200) is executed
search(200)
.then(function() { console.log('search1 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 2: Cancels search 1 and triggers its catch callback - httpRequest(2000) is called
search(2000)
.then(function() { console.log('search2 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

// Search 3: Cancels search 2 and triggers its catch callback - httpRequest(1000) is called
search(1000)
.then(function() { console.log('search3 resolved') })
.catch(function(err) { console.log('search3 rejected', err) });

In case of need, the catch callbacks can check err.message === '_cancelled_' to differentiate cancellation from other rejection reasons.

Answer №2

To create a factory function that encapsulates the search() method with the necessary cancellation behavior, you can define a custom function. It's important to note that while using Promise constructors is generally discouraged, in this specific scenario, it is required to maintain references to each reject() function in the pending set for early cancellation implementation.

function cancellable(fn) {
  const pending = new Set();

  return function() {
    return new Promise(async (resolve, reject) => {
      let settle;
      let result;

      try {
        pending.add(reject);
        settle = resolve;
        result = await Promise.resolve(fn.apply(this, arguments));
      } catch (error) {
        settle = reject;
        result = error;
      }

      // if this promise has not been cancelled
      if (pending.has(reject)) {
        // cancel the pending promises from calls made before this
        for (const cancel of pending) {
          pending.delete(cancel);

          if (cancel !== reject) {
            cancel();
          } else {
            break;
          }
        }

        settle(result);
      }
    });
  };
}

// internal API function
function searchImpl(timeout) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, timeout);
  });
}

// pass your internal API function to cancellable()
// and use the return value as the external API function
const search = cancellable(searchImpl);

search(200).then(() => {
  console.log('search1 resolved');
}, () => {
  console.log('search1 rejected');
});

search(2000).then(() => {
  console.log('search2 resolved');
}, () => {
  console.log('search2 rejected');
});

search(1000).then(() => {
  console.log('search3 resolved');
}, () => {
  console.log('search3 rejected');
});

search(500).then(function() {
  console.log('search4 resolved');
}, () => {
  console.log('search4 rejected');
});

This particular factory function leverages the ordering principle of Set to selectively cancel only the pending promises generated by previous calls before the current one that settles on a promise.


It's crucial to understand that invoking reject() doesn't halt any ongoing asynchronous operations triggered by creating the promise. Each HTTP request will proceed until completion, along with other internal functions invoked within search() prior to resolving the promise.

The purpose of cancellation() is solely to transition the internal state of the resultant promise from pending to rejected instead of fulfilled if a subsequent promise resolves first, ensuring that the appropriate handlers for promise resolution are executed by the consumer code.

Answer №3

Following a similar approach to PatrickRoberts, my recommendation would be to utilize a Map to manage a list of pending promises.

However, I propose avoiding the preservation of the reject callback outside of the promise constructor. Instead of explicitly rejecting an outdated promise, my suggestion is to simply disregard it. Create a promise wrapper that remains dormant without ever resolving or rejecting – essentially functioning as a stagnant promise object with no state alterations. This silent promise could serve as a universal placeholder for such occurrences.

Here's an illustration of how this concept can be implemented:

const delay = (timeout, data) => new Promise(resolve => setTimeout(() => resolve(data), timeout));
const godot = new Promise(() => null);

const search = (function () { // utilizing closure...
    const requests = new Map; // ...for shared variables
    let id = 1;
    
    return async function search() {
        let duration = Math.floor(Math.random() * 2000);
        let request = delay(duration, "data" + id); // Emulating an HTTP request
        requests.set(request, id++);
        let data = await request;
        if (!requests.has(request)) return godot; // Stays unresolved...
        for (let [pendingRequest, pendingId] of requests) {
            if (pendingRequest === request) break;
            requests.delete(pendingRequest);
            // Outputting demonstration purposes only. 
            // Not necessary in a real-world scenario:
            console.log("ignoring search " + pendingId);
        }
        requests.delete(request);
        return data;
    }    
})();

const reportSuccess = data => console.log("search resolved with " + data);
const reportError = err => console.log('search rejected with ' + err);

// Triggering searches at regular intervals.
// In practice, this could be initiated by key events.
// Each promise resolves after a random duration.
setInterval(() => search().then(reportSuccess).catch(reportError), 100);

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Learn how to incorporate a HTML page into a DIV by selecting an Li element within a menu using the command 'include('menu.html')'

I have a website with both 'guest' and 'user' content. To handle the different menus for logged-in and logged-out users, I created menuIn.html and menuOut.html files. These menus are included in my MainPage.php using PHP logic as sugges ...

I am facing an issue with Angular where the $http.get method is

It seems like there must be a small oversight causing this apparently simple problem. I have a function that interacts with the Spotify API to search for an artist. I know that by accessing the corresponding route using a standard URL, a result is returne ...

Looking for assistance with arranging and managing several containers, buttons, and modals?

My goal is to create a grid of photos that, when hovered over, display a button that can be clicked to open a modal. I initially got one photo to work with this functionality, but as I added more photos and buttons, I encountered an issue where the first b ...

Looping through an array in Vue using v-for and checking for a specific key-value pair

As I dive into my first Vue app, I've encountered a minor setback. Here's my query: How can I iterate through a list of dictionaries in Vue, specifically looping through one dictionary only if it contains a certain value for a given key? Provi ...

I am experiencing an issue where components are constantly re-rendering whenever I type something. However, I would like them to

Currently, I am in the process of developing a REACT application that takes two names, calculates a percentage and then generates a poem based on that percentage. The issue I am facing is that whenever I start typing in the input fields, the LovePercentCon ...

How to dynamically insert elements into the HTML page using Angular

When my page first loads, it looks like this <body> <div class="col-md-12" id="dataPanes"> <div class="row dataPane"> Chunk of html elements </div> </div> <div class"col-md-12 text-right"> <input type="butt ...

What could be causing the "undefined property read" error even though the data is clearly defined?

export async function getPrices() { const res = await fetch( "https://sandbox.iexapis.com/stable/crypto/BTCUSD/price?token=Tpk_1d3fd3aee0b64736880534d05a084290" ); const quote = await res.json(); return { props: { quote } }; } export de ...

Axios fails to capture and transmit data to the client's end

I created a backend using Express to retrieve Instagram feed images and then send their URLs to my front end, which is built with ReactJs. When I fetch the image URLs with instagram-node and send them to the front end, everything functions as expected. How ...

Having trouble with Angular's ng-class performance when dealing with a large number of elements in the

I've encountered a performance issue while working on a complex angular page. To demonstrate the problem, I've created a fiddle that can be viewed here. The main cause of the performance problem lies in the ng-class statement which includes a fu ...

Identify if a process operates synchronously or asynchronously

Can you identify in node.js, with the help of a function, if a method is synchronous or asynchronous? I am interested in creating a function that achieves the following: function isSynchronous(methodName) { //check if the method is synchronous, then ...

Executing secure journey within TypeScript

Just came across an enlightening article on Medium by Gidi Meir Morris titled Utilizing ES6's Proxy for secure Object property access. The concept is intriguing and I decided to implement it in my Typescript project for handling optional nested object ...

There seems to be an issue with the Link component from react-router-dom when used in conjunction with

I am currently utilizing react-router-dom along with Material-ui My main objective is to create a clickable row in a table that will lead to a specific path. Here is the code snippet: .map(client => ( <TableRow key={client.id} component={Link} to ...

Monitor the $scope within a factory by utilizing the $http service in AngularJS

I'm attempting to monitor a change in value retrieved from a factory using $http. Below is my factory, which simply retrieves a list of videos from the backend: app.factory('videoHttpService', ['$http', function ($http) { var ...

Working with time durations in Moment.js to manipulate datetime

How can I properly combine the variable secondsToMinutes with the startdate? The value of secondsToMinutes is "3:20" and the startDate is "2:00 PM". The desired endDate should be "2:03:20 PM". Despite my efforts, I keep encountering errors every time I at ...

What is the most effective way to reset the state array of objects in React JS?

I'm facing a challenge in resetting an array of objects without having to rewrite the initial state value again. Initial state const [state, setState] = useState([ {name: 'abc', age: 24}, {name: 'xyz', age: 20} ]) Is there a metho ...

React - Updating state from an external component

I realize that the question I am about to ask may go against some basic principles of using React... however, with this example, I hope someone can assist me in finding a solution to the problem I am currently facing. While this code snippet is not from m ...

Picked (chosen-js): Steps to deselect options using a variety of selectors

I am currently using the Chosen library on Vue and Webpack for my project. I have a scenario where I need to cancel a selection when multiple options are selected, but I am struggling to achieve this. Can anyone guide me on how to cancel a selected optio ...

Having difficulty viewing the images clearly

My JavaScript codes for scrolling images are not displaying all the images when viewed on Google Chrome. Is there a solution available? Below is the included JS code and CSS. data=[ [" images/img1.jpg"," Image1","pic01.jpg"], [" images/img2.jpg","Image2 " ...

JavaScript does not allow executing methods on imported arrays and maps

In my coding project, I created a map named queue in FILE 1. This map was fully built up with values and keys within FILE 1, and then exported to FILE 2 using module.exports.queue = (queue). Here is the code from FILE 1: let queue = new.Map() let key = &q ...

What is the best way to handle reading hefty files using the fs.read method and a buffer in JavaScript?

I'm currently delving into the world of javascript, and one of my go-to exercises when learning a new programming language is to create a hex-dump program. The main requirements for this program are: 1. to read a file provided through the command line ...