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

SOLVED: NextJS restricts plugins from modifying HTML to avoid unnecessary re-rendering

My current scenario is as follows: I am in the process of developing a website using NextJS (SSR) I have a requirement to load a script that will locate a div element and insert some HTML content (scripts and iframes) within it. The issue at hand: It se ...

Having trouble parsing the BODY of a NodeJs JSON post request?

I've been working on creating a basic API using nodeJS, but I've run into a problem while trying to post data. Below is my app.js file: const express = require('express'); const feedRoutes = require('./routes/feed'); const ...

AngularJS substitution with regular expressions

Looking to replace specific words in HTML content within <div> and <p> tags upon page load. Utilizing angularJS to achieve this task. This is my current function: var elementsList = e.find('p'); var regex = ('[^<>\& ...

Attach a Material-UI Popper component to a customized edge label in a React Flow

After finding inspiration from this particular example in the react-flow documentation, I decided to create my own customized version. It showcases a Material Ui Popper that appears when the edge is selected. However, my problem arises when attempting to z ...

What is the best way to transfer form data to another function without causing a page refresh?

Currently, I am in the process of developing a series of web applications using REACT JS. One specific app I am working on involves a modal that appears upon a state change and contains a form where users can input their name along with some related data. ...

A guide on enhancing Autocomplete Tag-it plugin with custom tags

Looking to expand the tags in the 'sampleTags' variable within this code snippet $(function () { var sampleTags = ['c++', 'java', 'php', 'coldfusion', 'javascript', 'asp', &apo ...

Facing some unexpected issues with integrating Django and AngularJS, unclear on the cause

In my simple Angular application, I encountered an issue. When I run the HTML file on its own in the browser, the variable name "nothing" is displayed as expected. However, once I integrate the app into Django by creating a dummy view that simply calls a t ...

Ways to update an angular page using the router without resorting to window.location.reload

I have a specific method for resetting values in a component page. The process involves navigating from the "new-edition" page to the "my-editions" component and then returning to the original location at "new-edition". I am currently using this approach ...

Using Javascript to attach <head> elements can be accomplished with the .innerHTML property, however, it does not work with XML child nodes

Exploring new ways to achieve my goal here! I want to include one JS and one jQuery attachment for each head section on every page of my static website. My files are: home.html <head> <script src="https://ajax.googleapis.com/ajax/libs/jquer ...

having trouble transferring data from one angular component to another

I've been attempting to send data from one component to another using the service file method. I've created two components - a login component and a home component. The goal is to pass data from the login component to the home component. In the l ...

What could be causing my state not to change in Nextjs even though I followed the quick start guide for Easy Peasy?

I recently encountered an issue while trying to implement easy peasy for global state management in my nextjs app. The problem I faced was that the state would only update when I changed pages, which seemed odd. To better understand what was going on, I de ...

What is the best way to create a time delay between two consecutive desktop screenshot captures?

screenshot-desktop is a unique npm API that captures desktop screenshots and saves them upon request. However, I encounter the need to call the function three times with a 5-second delay between each call. Since this API works on promises, the calls are e ...

Nuxt.js has exceeded the maximum call stack size

Recently, I started working on a nuxt.js/vue project using a pre-built starter template. However, I have been encountering numerous error messages such as "Maximum call stack size exceeded." It's quite challenging to pinpoint the exact source of these ...

What do you believe to be superior: Vue UI design?

Scenario: Application managing multiple user roles. A view with extensive conditional rendering in the template. a) Is it preferable to have numerous conditional statements in a single view's template? b) Should separate views be created for each ro ...

Angular: Disabling a button based on an empty datepicker selection

Can anyone help me figure out how to disable a button when the datepicker value is empty? I've tried using ngIf to check if the datepicker is empty and then disable the button, but it's not working. My goal is to make the button unclickable if th ...

Fetch request encountered a 500 error due to the absence of the 'Access-Control-Allow-Origin' header on the requested resource

Currently, I am in the process of developing a front-end web application using create-react-app and need to make a request to the ProPublica API. The fetch call code snippet is as follows: export const fetchSenators = () => dispatch => { fetch(' ...

Efficiently handling multiple form submissions using a single jQuery Ajax request

I'm working on a page that has 3-4 forms within divs, and I want to submit them all with just one script. Additionally, I want to refresh the content of the specific div where the form is located. However, I'm unsure of how to specify the current ...

Struggling with transferring form input data to a different file using JavaScript, Node.js, React.js, and Next.js

I've been struggling with writing form input to a separate file in JavaScript. I created a demo repo to showcase the issue I'm facing. Check it out here: https://github.com/projectmikey/projectmikey-cant-write-to-api-dir-stackoverflow Locally, t ...

Utilizing Angular for making API requests using double quotes

I am experiencing an issue with my service where the double quotation marks in my API URL are not displayed as they should be. Instead of displaying ".." around my values, it prints out like %22%27 when the API is called. How can I ensure that my ...

Exploring the significance of a super in Object-Oriented Programming within JavaScript

During my studies of OOP in JS, I encountered the super() method which serves to call the constructor of the parent class. It made me ponder - why is it necessary to invoke the parent class constructor? What significance does it hold for us? ...