post-css-position

A Step-by-Step Guide of JavaScript Promise in 30 Minutes

The Meaning of JavaScript Promise

Promise is a solution for asynchronous programming that is more reasonable and powerful than traditional solutions—callback functions and events. It was first proposed and implemented by the community. ES6 then wrote it into the language standard, unified its usage, and provided the Promise object natively.

The so-called Promise, simply, is a container that holds the result of an event that will end in the future (usually an asynchronous operation). Syntactically, Promise is an object from which you can get messages for asynchronous operations. Promise provides a unified API. Thus all asynchronous operations can be handled in the same way.

The Promise object has the following two characteristics:

  1. The state of the object is not affected by the outside world. A Promise object represents an asynchronous operation and has three states: pending (in progress), fulfilled (successful), and rejected (failed). Only the result of an asynchronous operation can determine the current state, and no other operation can change this state. This is also the origin of the name Promise. Its English meaning is “commitment”, indicating that other means can not change it.
  2. Once the state changes, it will not change any more, and this result can be obtained at any time. There are only two possibilities: from pending to fulfilled and from pending to rejected. As long as these two conditions occur, the state is solidified and will not change any more. It will always maintain this result, which is called “resolved”. If you add a callback function to a Promise after the change has happened, you will get the result immediately. This is completely different from the event. The characteristic of the event is that if you miss it and then listen, you will not get results.

Note that for the sake of convenience, from now on, the resolved refers only to the fulfilled state and does not include the rejected state.

With Promise objects, asynchronous operations can be expressed in a synchronous way, avoiding layered nested callback functions. In addition, Promise objects provide a uniform interface that makes it easier to control asynchronous operations.

There are also some disadvantages of Promise. First of all, it can’t be canceled. Once it is created, it will be executed immediately and cannot be cancelled midway. Second, if you don’t set the callback function, internal errors that threw by the Promise will not reflect to the outside. Third, when in the pending state, it is impossible to know which stage of the current progress (just started or is about to be completed).

Basic usage

According to ES6, a Promise object is a constructor that is used to generate a Promise instance.

The following code creates a Promise instance.

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* Asynchronous operation succeeded */){
    resolve(value);
  } else {
    reject(error);
  }
});

The Promise constructor accepts a function as a parameter whose two arguments are resolve and respectively reject. They are two functions that provided by the JavaScript engine.

The purpose of the resolve function is to change the state of the Promise object from “unfinished” to “successful” (i.e., from pending to resolved), when the asynchronous operation succeeds, and the result of the asynchronous operation is passed as a parameter. The reject function changes the state of the Promise object from “incomplete” to “failed” (i.e., from pending to rejected), which is called when the asynchronous operation fails, and the error reported by the asynchronous operation is passed as a parameter.

After the Promise instance is generated, you can use then method to specify callback functions for the resolved state and rejected state, respectively.

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

The then method can accept two callback functions as arguments. The first callback function is called when the state of the Promise object changes to resolved. And the second callback function is called when the state of the Promise object becomes rejected. Among them, the second function is optional and does not have to be provided. Both of these functions accept the value passed by the Promise object as their parameters.

Below is a simple example of a Promise object.

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

In the above code, the timeout method returns a Promise instance indicating the result that will occur after a while. After the specified time ( ms parameter) has elapsed, the state of the Promise instance becomes resolved, and the then callback function of the method binding is triggered.

Once a Promise is created, it will be executed immediately.

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

In the above code, Promise is executed immediately after it is newly created, so the output is first Promise. Then, the callback function specified by the then method will be executed after all the synchronization tasks of the current script have been executed, so the resolved is the final output.

The following is an example of loading an image asynchronously.

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

In the above code, use Promise to wrap an asynchronous image load. If the load is successful, the resolve method is called , otherwise the reject method is called .

Below is an example of an Ajax operation implemented with a Promise object.

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('Error: ', error);
});

In the above code, getJSON is a wrapper around the XMLHttpRequest object that is used to issue an HTTP request for JSON data and return a Promise object. It should be noted that, in the getJSON function, the functions resolve and reject are invoked with parameters.

If you call resolve and reject with arguments, their arguments are passed to the callback function specified by the then method. The argument to the reject function usually indicates the error thrown; the argument of the resolve function may be another Promise instance in addition to the normal value, as shown below.

const p1 = new Promise(function (resolve, reject) {
  // ...
});

const p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})

In the above code, p1 and p2 are both instances of Promise. But the resolve method of p2 uses p1 as a parameter. That is, the result of an asynchronous operation is to return another asynchronous operation.

Note that the state of p1 at this time is passed to p2. The state of p1 determines the state of p2. If the state of p1 is pending, then the callback function of p2 will wait for the state of p1 to change; if the state of p1 is already resolved or rejected, then the callback function of p2 will be executed immediately.

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

In the above code, p1 is a Promise, which changes to state rejected after 3 seconds. The state of p2 changes after 1 second, and the resolve method returns p1. Since p2 returns the Promise p1, the state of p2 is determined by the state of p1.

After one second, p2 returns p1, which is another promise. After another 2 seconds, the state of p1 becomes rejected, thus the callback function specified by the catch method is invoked.

Note that calling resolve or reject not does not terminate the execution of the parameter function of Promise.

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

In the above code, after the call resolve(1), the latter console.log(2) will still be executed, and will be printed first. This is because the immediately resolved Promise is executed at the end of this round of event loops, always after the synchronization task of this round.

In general, Promise’s mission is completed after the call of resolve or reject, and all subsequent operations should be placed in the then method, rather than directly written after the resolve or reject invocation. Therefore, it is best to add return statement before them so that there is no surprise.

new Promise((resolve, reject) => {
  return resolve(1);
  // The following statement will not be executed.
  console.log(2);
})

Promise.prototype.then()

A Promise instance has the then method, meaning the method is defined on the prototype object Promise.prototype. Its purpose is to add a callback function when a state change is made to a Promise instance. As mentioned earlier, the first argument to the then method is the resolved state’s callback function, and the second argument (optional) is the rejected state’s callback function.

The then method returns a new Promise instance (note that it is not the original Promise instance). So you can use chained writing, which means that you call another then method.

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

The above code uses the then method, which in turn specifies two callback functions. After the first callback function is completed, the returned result is passed as a parameter and passed to the second callback function.

With chained then, you can specify a set of callback functions that are called in order. At this time, the previous callback function, it is possible to return a Promise object (that is, there is an asynchronous operation), then the latter callback function will wait for the state of the Promise object to change before it will be executed.

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function (comments) {
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});

In the above code, the callback function specified by the first then method returns another Promise object. At this point, the callback function specified by the second then method will wait for the new Promise object’s state to change. If it becomes resolved, the first callback function is called, and if the state changes rejected, the second callback function is called.

If you use the arrow function, the above code can be written more concisely.

getJSON("/post/1.json").then(
  post => getJSON(post.commentURL)
).then(
  comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);

Promise.prototype.catch()

The Promise.prototype.catch method is an alias for .then(null, rejection) or .then(undefined, rejection), indicating the callback function when an error occurs.

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // Handling getJSON and the error that occurred when the previous callback function was run
  console.log('Error: ', error);
});

In the above code, the getJSON method returns a Promise object. If the object state changes resolved, the callback function specified by the then method is called. If the asynchronous operation throws an error, the state changes to rejected, and the callback function specified by the catch method is invoked. In addition, if an error is thrown during the execution of the callback function specified by the then method, the callback function of the catch method will catch that error.

p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// Equivalent to
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));

Below is an example.

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

In the above code, an error is thrown and caught by the callback function specified by the catch method. Note that the above writing is equivalent to the following two methods.

// Way one
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// Way two
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

Comparing the above two ways of writing, you can find out the role of the method reject, which is equivalent to throwing errors.

If the Promise state has changed resolved, throwing an error is invalid.

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

In the above code, Promise throws an error after the resolve statement and will not be caught, which is equivalent to not throwing. Because the state of the Promise has changed once, it will remain in that state forever and will not change anymore.

Promise object errors have a “bubble” nature and are passed back until they are captured. In other words, the error is always caught by the next catch statement.

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // Handle the error generated by the first three Promise
});

In the above code, there are a total of three Promise objects: one is returned by getJSON, and the others are generated by the two then methods. Errors thrown by any of them will be captured by the last catch.

In general, do not define a callback function for the Reject state in the then method (i.e., the second parameter), always use the catch method.

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

In the above code, the second method is better than the first one, on the grounds that the second method can capture errors in the execution of the previous then method, and is closer to the synchronous way (try/catch). Therefore, it is recommended to always use the catch method instead of specifying the second parameter of the then method.

Unlike traditional try/catch code blocks, if you do not specify a callback function for error handling, the error thrown by the Promise object will not be passed to the outer code. I.e., there will be no response.

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // The following line will report an error because x is not declared
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

In the above code, the Promise object generated by the function someAsyncThing has a syntax error inside. When the browser runs to this line, an error message will be printed ReferenceError: x is not defined, but the script execution will not be terminated. And it will still output 123 after 2 seconds. That is to say, errors inside Promise will not affect the code outside Promise. The popular saying is “Promise will eat the error”.

This script is placed on the server and the exit code is 0 (i.e., the execution is successful). However, Node has an unhandledRejection event that listens for uncaught reject errors. The above script will trigger the listener function for this event, which can throw an error in the listener function.

process.on('unhandledRejection', function (err, p) {
  throw err;
});

In the above code, the listen function of the event unhandledRejection has two parameters, the first one is the error object, and the second is the Promise instance that reports the error, which can be used to understand the environment information of the error.

Note that Node has plans to abolish the unhandledRejection event in the future. If there is an uncaught error inside the Promise, the process is terminated directly and the exit code of the process is not 0.

Look at the example below.

const promise = new Promise(function (resolve, reject) {
  resolve('ok');
  setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test

In the above code, Promise specifies to throw an error in the next round of “event loop”. By that time, Promise’s operation was over, so this error was thrown outside the Promise function and bubbled to the outermost layer, making it an uncaught error.

It is generally recommended that the Promise object be followed by a catch method that handles errors that occur inside the Promise.

The catch method returns a Promise object, so you can then call the then method later.

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // The following line will report an error because x is not declared
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on

The above code runs the callback function specified by the catch method, and then runs the callback function specified by the latter then method. If no error is reported, the catch method will be skipped .

Promise.resolve()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// carry on

The above code skips the catch method and executes the then method directly because there is no error. At this point, if the then method is reported incorrectly, it has nothing to do with the previous catch.

In the catch method, you can throw another error.

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // The following line will report an error because y is not declared  
  y + 2;
}).then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]

In the above code, the catch method throws an error because there is no other catch method behind, causing the error not to be caught or passed to the outer layer. If you rewrite it, the result will be different.

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  y + 2;
}).catch(function(error) {
  console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

In the above code, the second catch method is used to catch the error thrown by the previous catch method.

Promise.prototype.finally()

The finally method is used to specify the action that will be performed regardless of the final state of the Promise object. This method is standard to the ES2018.

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

In the above code, regardless of the final state of a promise, the callback function specified by the finally method is executed after the execution of the callback function specified by then or catch.

Here’s an example where the server uses Promise to process the request and then uses the finally method to turn off the server.

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

The finally method’s callback function does not accept any arguments, which means there is no way to know if the previous Promise state is fulfilled or rejected. This shows that the operations in the finally method should be state-independent and independent of the execution result of the Promise.

Essentially, finally is a special case of the then method.

promise
.finally(() => {
  // Statements
});

// 等同于
promise
.then(
  result => {
    // Statements
    return result;
  },
  error => {
    // Statements
    throw error;
  }
);

In the above code, if you don’t use a finally method, the same statement needs to be written twice for both success and failure. With the finally method, you only need to write it once.

Its implementation is also very simple.

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

In the above code, the callback function is executed regardless of whether the previous Promise is fulfilled or rejected.

As you can see from the above implementation, the finally method will always return the original value.

// resolve undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 2
Promise.resolve(2).finally(() => {})

// reject undefined
Promise.reject(3).then(() => {}, () => {})

// reject 3
Promise.reject(3).finally(() => {})

Promise.all()

The Promise.all method is used to wrap multiple Promise instances into a new Promise instance.

const p = Promise.all([p1, p2, p3]);

The above code, the Promise.all method takes an array as a parameter, p1p2p3 are Promise instances. If not, the Promise.resolve method will be called first, converting the parameter into Promise instance. (The argument to the Promise.all method can be not an array, but it must have an Iterator interface, and each member returned is a Promise instance.)

The state of p is determined by p1p2 and p3. There are two situations.

  1. p1p2 and p3 are all turned into a state fulfilled, the state of p will become fulfilled. In this case, the return values of p1p2 and p3 consist an array, and then passed to the callback function of p.
  2. One of the states of p1p2 and p3 being changed to rejected, the state of p becomes rejected. The return value of the first promise instance that changed to reject, is passed to the callback function of p.

The following is a concrete example.

// Generate an array of Promise objects
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

In the above code, promises is an array of 6 Promise instances. Only all the states of these 6 instances becomes fulfilled, or one of them changes to rejected, the callback functions after the method Promise.all will be invoked.

Here’s another example.

const databasePromise = connectDatabase();

const booksPromise = databasePromise
  .then(findAllBooks);

const userPromise = databasePromise
  .then(getCurrentUser);

Promise.all([
  booksPromise,
  userPromise
])
.then(([books, user]) => pickTopRecommendations(books, user));

In the above code, the booksPromise and userPromise are two asynchronous operations, and the callback function pickTopRecommendations will only be triggered when their results are returned.

Note that if the Promise instance as a parameter defines the catch method itself, then once it is rejected, it does not trigger the catch method of Promise.all().

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('error');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 'error']

In the above code, p1 will resolvedp2 will first rejected. But p2 has its own catch method, which returns a new instance of Promise. In fact, p2 is pointing to this instance. After the instance executes the catch method, it will also become resolved, causing both instances in the Promise.all() method to be resolved. So the callback function specified by the then method will be called instead of the callback function specified by the catch method.

If p2 do not own its catch method, it will call the catch method of Promise.all().

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('error');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: error

Promise.race()

The Promise.race method is also used to wrap multiple Promise instances into a new Promise instance.

const p = Promise.race([p1, p2, p3]);

In the above code, as long as one of  p1p2 and p3 changes state, the state of p will change accordingly. The return value of the Promise instance that was first changed, is passed to the callback function of p.

The parameters of the Promise.race method are the same as the Promise.all method. If it is not a Promise instance, the Promise.resolve method described below will be called first, and the parameter will be converted to a Promise instance for further processing.

The following is an example. If no result is obtained within the specified time, the status of Promise is changed to reject, otherwise it becomes resolve.

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p.then(console.log)
 .catch(console.error);

In the above code, if the fetch method cannot return the result within 5 seconds , the state of the variable p will change to rejected, triggering the callback function specified by the catch method.

Promise.resolve()

Sometimes it’s necessary to turn an existing object into a Promise object. The Promise.resolve method does this.

const jsPromise = Promise.resolve($.ajax('/whatever.json'));

The above code turns the deferred object generated by jQuery into a new Promise object.

Promise.resolve is equivalent to the following notation.

Promise.resolve('foo')
// Equivalent to
new Promise(resolve => resolve('foo'))

The parameters of the Promise.resolve method are divided into four cases.

(1) The parameter is a Promise instance.

If the argument is a Promise instance, then the instance will be returned unchanged without any modifications.

(2) The parameter is an thenable object.

A thenable object refers to an object with a then method, such as the object below.

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

The Promise.resolve method will turn this object into a Promise object and then immediately execute the thenable object’s then method.

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});

In the above code, after the execution of the thenmethod of the thenable object, the state of the p1 object becomes resolved. And the callback function specified by the last then method is executed immediately. The number 42 is printed.

(3) The parameter is not an object with a then method, or it is not an object at all.

If the argument is a primitive value, or an object that does not have a then method, the Promise.resolve method returns a new Promise object with a status of resolved.

const p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

The above code generates an instance of a new Promise object p. Since the string Hello is not an asynchronous operation (the string object does not have the then method), the state of the returned Promise instance is resolved after its generation. So the callback function will be executed immediately. The parameters of the Promise.resolve method are passed to the callback function at the same time.

(4) Without any parameters

The Promise.resolve() method returns a Promise object with a state resolved, when no parameter is provided.

So, if you want to get a Promise object, the more convenient way is to call the Promise.resolve() method directly .

const p = Promise.resolve();

p.then(function () {
  // ...
});

The variable p in the above code is a Promise object.

It should be noted that the immediate resolve() Promise object is executed at the end of this round of the “event loop”, not at the beginning of the next round of “event loops”.

setTimeout(function () {
  console.log('three');
}, 0);

Promise.resolve().then(function () {
  console.log('two');
});

console.log('one');

// one
// two
// three

In the above code, setTimeout(fn, 0) is executed at the beginning of the next round of “event loop”. Promise.resolve() is executed at the end of this round of “event loop”. And console.log('one') is executed immediately.

Promise.reject()

The Promise.reject(reason) method also returns a new Promise instance with a status of rejected.

const p = Promise.reject('Something went wrong.');
// Equivalent to
const p = new Promise((resolve, reject) => reject('Something went wrong.'))

p.then(null, function (s) {
  console.log(s)
});
// Something went wrong.

The above code generates an instance of a Promise object with a status of rejected, and the callback function executes immediately.

Note that the parameters of the Promise.reject() method will be used as the reason for the subsequent reject method. This is inconsistent with the Promise.resolve method.

const thenable = {
  then(resolve, reject) {
    reject('Something went wrong.');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true

In the above code, the parameter of the Promise.reject method is a thenable object. After the execution, the parameter of the latter catch method is not the “error” string that is thrown by reject, but the thenable object.

Application

Loading Image

We can write the image load Promise. Once the load is complete, the state of the Promise changed.

const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

The Combination of the Generator Function and Promise

When using the Generator function, we usually return a Promise object when it encounters an asynchronous operation .

function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);

In the Generator function g of the above code , there is an asynchronous operation getFoo that returns a Promise object. The function run is used to process this Promise object and call the next method next.

Promise.try()

In actual development, I often encounter a situation: I don’t know or don’t want to distinguish, whether the function f is a synchronous function or an asynchronous operation, but I want to use Promise to handle it. Because in this way, regardless of whether or not f contains asynchronous operations, the then method is used to specify the next step, and the catch method is used to handle the error thrown by f. Generally speaking, the following way is used.

Promise.resolve().then(f)

One disadvantage of the above is that if f is a synchronous function, it will be executed at the end of this round of event loops.

const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now

In the above code, the function f is synchronous, but after being wrapped with Promise, it becomes asynchronous.

So is there a way to have synchronous functions execute synchronously, asynchronous functions execute asynchronously, and have them have a uniform API? The answer is yes, and there are two ways to write it. The first way is to write with a async function.

const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next

In the above code, the second line is an anonymous function that is executed immediately, and the async function inside will be executed immediately , so if f is synchronous, it will get the result of synchronization; if f is asynchronous, you can use then to specify next step:

(async () => f())()
.then(...)

It should be noted that async () => f() will eat the errors threw by by f(). So if you want to catch errors, use the promise.catch method.

(async () => f())()
.then(...)
.catch(...)

The second way of writing is to use new Promise().

const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

The above code is also executed using an anonymous function that is executed immediately. In this case, the synchronization function is also executed synchronously.

Since this is a very common requirement, there is now a proposal to provide a Promise.try way to replace the above.

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

Since Promise.try provides a unified processing mechanism for all operations, if you want to use the then method to manage process, it is best to use Promise.try to wrap the function. There are many benefits to this, one of which is to manage exceptions.

function getUsername(userId) {
  return database.users.get({id: userId})
  .then(function(user) {
    return user.name;
  });
}

In the above code, database.users.get() returns a Promise object. If an asynchronous error is thrown, it can be captured by the catch method, as written below.

database.users.get({id: userId})
.then(...)
.catch(...)

But database.users.get() may also throw a synchronization error (such as a database connection error, depends on the specific implementation), then you have to use try...catch to capture.

try {
  database.users.get({id: userId})
  .then(...)
  .catch(...)
} catch (e) {
  // ...
}

The above code is awkward, and you can use promise.try() to capture all synchronous and asynchronous errors.

Promise.try(() => database.users.get({id: userId}))
  .then(...)
  .catch(...)

In fact, it Promise.try simulates the trycode block, just like promise.catch simulates the catch code block.

Closing Words

JavaScript promise is a useful tool for performing asynchronous tasks. Every Web developer should master it. We have talked many aspects of the Promise in this article, but I suggest you practise as much as you can.

Some concepts has been introduced, e.g., function, object, in our previous posts:

If you encounter any issue, please leave us a comment!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *