Promise - rapidly access data from a function that resolves a Promise

Could someone suggest a technique for instantly extracting data from a function that returns a Promise?

For example, in my simplified scenario, I have an AJAX preloader like this:

loadPage("index.html").then(displayPage);

If the page being loaded is large, I need to be able to monitor its progress and maybe even halt the process by using XHR abort() later on.

Prior to utilizing Promises, my loadPage function would return an id that allowed me to achieve this:

var loadPageId = loadPage("index.html",displayPage);
...
doSomething(loadPageId);
cancelLoadPage(loadPageId);

In my new Promise-based implementation, I assume that cancelLoadPage() would reject() the initial loadPage() Promise.

I've examined several possible solutions, none of which appeal to me. Is there a widely accepted approach to solve this?

Answer №1

First and foremost, let's address the bounty note.

[I hope to award points to someone who provides more insight than just saying "Avoid using promises"... ]

Unfortunately, the bottom line here is simply: "Avoid using promises". ES6 Promises present three potential states (from a user perspective): Pending, Resolved, and Rejected (the names may vary slightly).

You cannot access the inner workings of a promise to determine what has been completed and what hasn't – at least not with native ES6 promises. Some limited progress was made on promise notifications in other frameworks, but this functionality didn’t make it into the ES6 specification; therefore, utilizing it would be unwise, even if you manage to find an implementation for it.

A promise's purpose is to represent an asynchronous task that will occur in the future; standalone, it isn’t suitable for this use case. What you likely need is more akin to an event publisher - and even that operates asynchronously, not synchronously.

There isn’t a reliable way for you to synchronously retrieve a value from an asynchronous call, especially within JavaScript. One significant reason for this is that a well-designed API will always opt for asynchrony when feasible.

Consider the following scenario:

const promiseValue = Promise.resolve(5)
promiseValue.then((value) => console.log(value))
console.log('test')

If this promise were resolved synchronously (since we know the value ahead of time), what outcome would you anticipate? A typical expectation might be:

> 5
> test

However, the actual sequence is as follows:

> test
> 5

This discrepancy arises because while Promise.resolve() is a synchronous operation that resolves an already-resolved Promise, then() consistently functions asynchronously; this is a fundamental guarantee of the specification, serving to enhance code understanding - envision the complications that could arise from blending synchronous and asynchronous promises.

This principle holds true for all asynchronous tasks: any potentially asynchronous action in JavaScript definitively operates asynchronously. Consequently, no synchronous introspection can be performed within any JavaScript-provided API.

This doesn’t imply that developing a wrapper around a request object is impossible, such as:

function makeRequest(url) {
  const requestObject = new XMLHttpRequest()
  const result = {

  }

  result.done = new Promise((resolve, reject) => {
    requestObject.onreadystatechange = function() {
      ..
    }
  })
  requestObject.open(url)
  requestObject.send()
  return requestObject
}

Nonetheless, this approach quickly becomes convoluted, necessitating some form of asynchronous callback to operate effectively. Challenges arise particularly when implementing Fetch. Moreover, bear in mind that Promise cancellation hasn’t been standardized yet. Refer to here for further insights on this aspect.

TL:DR: It’s impossible to introspect synchronously on any asynchronous task in JavaScript, and utilizing a Promise for such endeavors is ill-advised. You’re incapable of synchronously displaying details about an ongoing request, for instance. In alternative programming languages, attempting this would demand blocking or inadvertently triggering a race condition.

Answer №2

In the realm of Angular development, utilizing the timeout parameter in conjunction with the $http service is a viable option for effectively canceling an ongoing HTTP request.

An illustrative example in TypeScript:

interface ReturnObject {
  cancelPromise: ng.IPromise;
  httpPromise: ng.IHttpPromise;
}

@Service("moduleName", "aService")
class AService() {

  constructor(private $http: ng.IHttpService
              private $q: ng.IQService) { ; }

  doSomethingAsynch(): ReturnObject {
    var cancelPromise = this.$q.defer();
    var httpPromise = this.$http.get("/blah", { timeout: cancelPromise.promise });
    return { cancelPromise: cancelPromise, httpPromise: httpPromise };
  }
}

@Controller("moduleName", "aController")
class AController {

  constructor(aService: AService) {
    var o = aService.doSomethingAsynch();

    var timeout = setTimeout(() => {
      o.cancelPromise.resolve();
    }, 30 * 1000);

    o.httpPromise.then((response) => {
      clearTimeout(timeout);
      // perform necessary tasks
    }, (errorResponse) => {
      // handle errors
    });
  }
}

This method already yields an object containing two promises, making it convenient to incorporate any synchronous operation return data within that parameter.

If you have specific requirements for returning synchronous data from this method, articulating the nature of this data could aid in recognizing a suitable pattern. Consider whether another method may suffice for executing synchronous operations prior to or alongside your asynchronous task.

Answer №3

You have the option to implement this functionality, although it may involve using unconventional methods. It is important to note that exporting the resolve and reject methods is typically viewed as a promise anti-pattern (indicating that promises should not be used in such scenarios). Below, you will find an example utilizing setTimeout which could achieve the desired outcome without the need for workarounds.

let xhrRequest = (path, data, method, success, fail) => {
  const xhr = new XMLHttpRequest();

  // Alternatively, this could be structured differently for flexibility
  switch (method) {
    case 'GET':
      xhr.open('GET', path);
      xhr.onload = () => {
          if (xhr.status < 400 && xhr.status >= 200) {
            success(xhr.responseText);
            return null;
          } else {
            fail(new Error(`Server responded with a status of ${xhr.status}`));
            return null;
          }
        };
        xhr.onerror = () => {
          fail(networkError);
          return null;
        }
        xhr.send();
        return null;
      }

      return xhr;

    case 'POST':
      // etc.
      return xhr;

    // and so on...
};

// Works with any function accepting success and fail callbacks
class CancellablePromise {
  constructor (fn, ...params) {
    this.promise = new Promise((res, rej) => {
      this.resolve = res;
      this.reject = rej;
      fn(...params, this.resolve, this.reject);
      return null;
    });
  }
};

let p = new CancellablePromise(xhrRequest, 'index.html', null, 'GET');

p.promise.then(loadPage).catch(handleError);

// Times out after 2 seconds
setTimeout(() => { p.reject(new Error('timeout')) }, 2000);

// For an alternative version that notifies users of prolonged wait times,
// Note: This can also be achieved with standard vanilla promises

let timeoutHandle = setTimeout(() => {
  // Avoid using alerts for real applications, but this serves as an example
  alert('Apologies for the delayed page loading.');
}, 2000);

p.promise.then(() => clearTimeout(timeoutHandle));

Answer №4

Promises are truly remarkable. There is no reason why you cannot manage this situation using promises. I can think of three ways to approach it.

  1. The easiest method is to handle it within the executor function. If you need to cancel the promise (e.g., due to a timeout), you can set a timeout flag in the executor and activate it using
    setTimeout(_ => timeout = true, 5000)
    . Then, only resolve or reject the promise if the timeout flag is false. For example, use !timeout && resolve(res) or !timeout && reject(err). This way, the promise will remain unresolved if a timeout occurs, and the onfulfillment and onreject functions in the then stage will not be called.
  2. The second approach is similar to the first, but instead of using a flag, you simply invoke reject with an appropriate error description after the timeout. You can then handle the result at the then or catch stages.
  3. If you want to bring the id of your asynchronous operation into the synchronous world, you can achieve that as well.

In such cases, you have to convert the async function into a promise yourself. Let's consider an example where we have an async function that returns double the value of a number:

function doubleAsync(data, cb){
  setTimeout(_ => cb(false, data * 2),1000);
}

To use promises in this scenario, we typically need to create a promisifier function. This function takes our async function and transforms it into another function that returns a promise when executed with the data. Here's how the promisifier function looks:

function promisify(fun){
  return (data) => new Promise((resolve, reject) => fun(data, (err, res) => err ? reject(err) : resolve(res)));
}

Let's see how these functions work together:

function promisify(fun){
  return (data) => new Promise((resolve, reject) => fun(data, (err, res) => err ? reject(err) : resolve(res)));
}

function doubleAsync(data, cb){
  setTimeout(_ => cb(false, data * 2),1000);
}

var doubleWithPromise = promisify(doubleAsync);
doubleWithPromise(100).then(v => console.log("The asynchronously obtained result is: " + v));

Now, our doubleWithPromise(data) function returns a promise, and we can chain a then stage to access the returned value.

However, if you require not only a promise but also the id of your async function, you can easily achieve that. Your promisified function should return an object with two properties: a promise and an id. Here's how you can do it:

This time, our async function will randomly return a result within 0-5 seconds. We will receive its result.id synchronously along with the result.promise. If the promise fails to resolve within 2.5 seconds, the id can be used to cancel it. Any duration above 2501 milliseconds on the console log will indicate that the promise has been canceled.

function promisify(fun){
  return function(data){
           var result = {id:null, promise:null};       // template return object
           result.promise = new Promise((resolve, reject) => result.id = fun(data, (err, res) => err ? reject(err) : resolve(res)));
           return result;
         };
}

function doubleAsync(data, cb){
  var dur = ~~(Math.random() * 5000);                    // return the double of the data within 0-5 seconds.
  console.log("Resolve in " + dur + " milliseconds");
  return setTimeout(_ => cb(false, data * 2), dur);
}

var doubleWithPromise = promisify(doubleAsync),
       promiseDataSet = doubleWithPromise(100);

setTimeout(_ => clearTimeout(promiseDataSet.id), 2500); // give 2.5 seconds to the promise to resolve or cancel it.
promiseDataSet.promise
              .then(v => console.log("The asynchronously obtained result is: " + v));

Answer №5

To cancel the read operation of a stream, you can utilize the fetch() method along with Response.body.getReader(). This process involves calling the .read() function which then returns a ReadableStream. The ReadableStream includes a method called cancel, which in turn returns a Promise upon cancellation.

// 58977 bytes of text, 59175 total bytes
var url = "https://gist.githubusercontent.com/anonymous/"
          + "2250b78a2ddc80a4de817bbf414b1704/raw/"
          + "4dc10dacc26045f5c48f6d74440213584202f2d2/lorem.txt";
var n = 10000;
var clicked = false;
var button = document.querySelector("button");
button.addEventListener("click", () => {clicked = true});

fetch(url)
.then(response => response.body.getReader())
.then(reader => {
  var len = 0;
  reader.read().then(function processData(result) {
    if (result.done) {
      // do stuff when `reader` is `closed`
      return reader.closed.then(function() {
        return "stream complete"
      });
    };
    if (!clicked) {
      len += result.value.byteLength;
    }
    // cancel stream if `button` clicked or 
    // to bytes processed is greater than 10000
    if (clicked || len > n) {
      return reader.cancel().then(function() {
        return "read aborted at " + len + " bytes"
      })
    }
    console.log("len:", len, "result value:", result.value);
    return reader.read().then(processData)
  })
  .then(function(msg) {
    alert(msg)
  })
  .catch(function(err) {
    console.log("err", err)
  })
});
<button>click to abort stream</button>

Answer №6

My current approach involves the following steps:

var optionalReturnsObject = {};
executeRequest(dataToSend, optionalReturnsObject ).then(handleAsyncResponse);
console.log("Instant data returned:", optionalReturnsObject ); 

I find this method beneficial as it allows any team member to easily utilize it:

executeRequest(data).then(...);

There is no need for them to be concerned about the returns object. Experienced users can understand what is happening based on the definitions provided.

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

Node.js can be utilized to make multiple API requests simultaneously

I am facing an issue while trying to make multiple external API calls within a for loop. Only one iteration from the for loop is sending back a response. It seems that handling multi-API requests this way is not ideal. Can you suggest a better approach fo ...

Using the CSRF token in an AJAX modal within Codeigniter 4 for efficient reuse

Situation: I am currently in the process of developing a CMS system and I have the intention to incorporate categories for various objects such as pages, posts, media, etc. To achieve this, I have implemented an HTML form within a Bootstrap modal that is ...

Tips for utilizing a function in Python within two separate classes that have distinct return parameters

I came across the following code snippet: class one(xyz): def __init__(self,...): .... def myfunction(self,p,q): .... self.dummy_ = 1 self.corr_ = 3 return self class two(one): .... class three(one): In this sc ...

Is it better to utilize Head.js or simply use minified scripts before the closing </body> tag along with scripts directly in the

I am facing an optimization challenge with my website, as it currently utilizes 2 rather large JavaScript resources: application.js (which includes minified jQuery, jQuery-ui, underscore-js, and some shared scripts totaling 120KB) A controller-specific f ...

`How can I activate a modal window when an option is chosen?`

Utilize the select tag and modal window tag for this example. <select id="selectBox"> <option value="0">Select</option> <option value="1">Yes</option> <option value="2">No</option> </select> <div id ...

javascript categorize data by key and display in a tabular format

I have a Client ID and gender information available. Shown below is a JSON response along with a JavaScript function to display the data in a table format. The JSON response structure is as follows: studies = [{ "id": { "Value&qu ...

How can I integrate NIB into a project created with WebStorm 8 using node.js, Express, Jade, and Stylus?

Starting off with node.js, Express, Jade, Styl and NIB in WebStorm 8 has been a bit of a challenge. Unfortunately, WebStorm does not natively support NIB, so I have been looking into how to manually add it. Here is the sample app.js generated by WebStorm: ...

Changing the background color of a button

In the HEADER section, there are 4 buttons (B1, B2, B3, B4), each leading to a different page in the same tab. The background color of the buttons is set to WHITE. When a specific button is clicked, the entire page reloads and redirects to the correspond ...

Utilizing Ajax in Magento Module

I'm a beginner in magento module development and I'm looking to make an ajax call from the magento backend. I've created a module, which you can see in the screenshot https://i.sstatic.net/CnThl.png My fields look like this: <tpropay tr ...

Implement Material UI TextField to ensure all required fields are properly formatted

Looking to customize the underline border color of TextFields marked as mandatory within a form using react-hooks-form. I understand that I need to define a style for these fields, but I'm struggling with where to start... This is the current code s ...

Passing PHP data through AJAX to a JSON object and displaying it in

I've been making progress with the Ajax method, but I keep encountering some minor issues along the way. One problem I'm facing is trying to implement a display-comment functionality. When the user clicks on a link, I want a modal window to open ...

Using jQuery AJAX to show user input field information

I am currently experimenting with using jQuery ajax to gather data from a form and then display the data using print_r. Here is an example of my index.php file: <!DOCTYPE html> <html> <head> <title>CRUD application wit ...

Programmatically simulate a text cursor activation as if the user has physically clicked on the input

I have been attempting to input a value into a text field using JavaScript, similar to the Gmail email input tag. However, I encountered an issue with some fancy animations that are tied to certain events which I am unsure how to trigger, illustrated in th ...

What is the best way to assign an ID to a specific HTML element within NetSuite's Document Object Model

Attempting to utilize jQuery in NetSuite to assign a value to an element for testing purposes, but struggling to locate the HTML DOM ID of certain custom drop-down lists. It's not the internal ID. For example: <input type="text" id="soid"> Wh ...

When clicking on a link in React, initiate the download of a text file

Usually, I can use the following line to initiate the download of files: <a href={require("../path/to/file.pdf")} download="myFile">Download file</a> However, when dealing with plain text files like a .txt file, clicking on ...

Can a layer be sliced to create a text-shaped cutout?

I want to achieve a cool text effect where the background is visible through the letters. I have explored options with background images and colors, but I haven't found any examples where the underlying layer is revealed. Is this even possible? Imag ...

Storing and Manipulating a JavaScript Object with Vuex: A New Perspective

Consider a hypothetical JavaScript object class like this: class Car { var engineTurnedOn = false; ... public turnEngineOn() { engineTurnedOn = true } } If I want to turn the engine on, should I create an action called 'turnEngineOn&ap ...

Creating a regular expression to capture a numerical value enclosed by different characters:

export interface ValueParserResult { value: number, error: string } interface subParseResult { result: (string | number) [], error: string } class ValueParser { parse(eq: string, values: {[key: string] : number}, level?: number) : ValueParse ...

Embeddable javascript code can be enhanced by incorporating references into the header

I have developed a basic calculator application using HTML, JavaScript, JQuery, and CSS. My intention is to allow others to embed this calculator app on their own web pages by utilizing a file named generate.js. Within this file, there are several documen ...

When working on a REST APIs project with ReactJS fetch and NodeJS, I am encountering difficulties in reading authorization headers on the server side

I'm having issues retrieving headers on the server side using ReactJS fetch to call an API on the front end. In my old project, this functionality was working perfectly fine. I'm puzzled as to why it's not working now, especially since I hav ...