The Promise class is something that exists in many modern JavaScript engines and can be easily polyfilled. The main motivation for promises is to bring synchronous style error handling to Async / Callback style code.
Callback style code
In order to fully appreciate promises let's present a simple sample that proves the difficulty of creating reliable Async code with just callbacks. Consider the simple case of authoring an async version of loading JSON from a file. A synchronous version of this can be quite simple:
import fs =require('fs');functionloadJSONSync(filename:string) {returnJSON.parse(fs.readFileSync(filename));}// good json fileconsole.log(loadJSONSync('good.json'));// non-existent file, so fs.readFileSync failstry {console.log(loadJSONSync('absent.json'));}catch (err) {console.log('absent.json error',err.message);}// invalid json file i.e. the file exists but contains invalid JSON so JSON.parse failstry {console.log(loadJSONSync('invalid.json'));}catch (err) {console.log('invalid.json error',err.message);}
There are three behaviors of this simple loadJSONSync function, a valid return value, a file system error or a JSON.parse error. We handle the errors with a simple try/catch as you are used to when doing synchronous programming in other languages. Now let's make a good async version of such a function. A decent initial attempt with trivial error checking logic would be as follows:
import fs =require('fs');// A decent initial attempt .... but not correct. We explain the reasons belowfunctionloadJSON(filename:string,cb: (error:Error, data:any) =>void) {fs.readFile(filename,function (err, data) {if (err) cb(err);elsecb(null,JSON.parse(data)); });}
Simple enough, it takes a callback, passes any file system errors to the callback. If no file system errors, it returns the JSON.parse result. A few points to keep in mind when working with async functions based on callbacks are:
Never call the callback twice.
Never throw an error.
However, this simple function fails to accommodate for point two. In fact, JSON.parse throws an error if it is passed bad JSON and the callback never gets called and the application crashes. This is demonstrated in the below example:
import fs =require('fs');// A decent initial attempt .... but not correctfunctionloadJSON(filename:string,cb: (error:Error, data:any) =>void) {fs.readFile(filename,function (err, data) {if (err) cb(err);elsecb(null,JSON.parse(data)); });}// load invalid jsonloadJSON('invalid.json',function (err, data) {// This code never executesif (err) console.log('bad.json error',err.message);elseconsole.log(data);});
A naive attempt at fixing this would be to wrap the JSON.parse in a try catch as shown in the below example:
import fs =require('fs');// A better attempt ... but still not correctfunctionloadJSON(filename:string,cb: (error:Error) =>void) {fs.readFile(filename,function (err, data) {if (err) {cb(err); }else {try {cb(null,JSON.parse(data)); }catch (err) {cb(err); } } });}// load invalid jsonloadJSON('invalid.json',function (err, data) {if (err) console.log('bad.json error',err.message);elseconsole.log(data);});
However, there is a subtle bug in this code. If the callback (cb), and not JSON.parse, throws an error, since we wrapped it in a try/catch, the catch executes and we call the callback again i.e. the callback gets called twice! This is demonstrated in the example below:
import fs =require('fs');functionloadJSON(filename:string,cb: (error:Error) =>void) {fs.readFile(filename,function (err, data) {if (err) {cb(err); }else {try {cb(null,JSON.parse(data)); }catch (err) {cb(err); } } });}// a good file but a bad callback ... gets called again!loadJSON('good.json',function (err, data) {console.log('our callback called');if (err) console.log('Error:',err.message);else {// let's simulate an error by trying to access a property on an undefined variablevar foo;// The following code throws `Error: Cannot read property 'bar' of undefined`console.log(foo.bar); }});
This is because our loadJSON function wrongfully wrapped the callback in a try block. There is a simple lesson to remember here.
Simple lesson: Contain all your sync code in a try catch, except when you call the callback.
Following this simple lesson, we have a fully functional async version of loadJSON as shown below:
import fs =require('fs');functionloadJSON(filename:string,cb: (error:Error) =>void) {fs.readFile(filename,function (err, data) {if (err) returncb(err);// Contain all your sync code in a try catchtry {var parsed =JSON.parse(data); }catch (err) {returncb(err); }// except when you call the callbackreturncb(null, parsed); });}
Admittedly this is not hard to follow once you've done it a few times but nonetheless it’s a lot of boiler plate code to write simply for good error handling. Now let's look at a better way to tackle asynchronous JavaScript using promises.
Creating a Promise
A promise can be either pending or fulfilled or rejected.
Let's look at creating a promise. It's a simple matter of calling new on Promise (the promise constructor). The promise constructor is passed resolve and reject functions for settling the promise state:
constpromise=newPromise((resolve, reject) => {// the resolve / reject functions control the fate of the promise});
Subscribing to the fate of the promise
The promise fate can be subscribed to using .then (if resolved) or .catch (if rejected).
constpromise=newPromise((resolve, reject) => {resolve(123);});promise.then((res) => {console.log('I get called:', res ===123); // I get called: true});promise.catch((err) => {// This is never called});
constpromise=newPromise((resolve, reject) => {reject(newError("Something awful happened"));});promise.then((res) => {// This is never called});promise.catch((err) => {console.log('I get called:',err.message); // I get called: 'Something awful happened'});
TIP: Promise Shortcuts
Quickly creating an already resolved promise: Promise.resolve(result)
Quickly creating an already rejected promise: Promise.reject(error)
Chain-ability of Promises
The chain-ability of promises is the heart of the benefit that promises provide. Once you have a promise, from that point on, you use the then function to create a chain of promises.
If you return a promise from any function in the chain, .then is only called once the value is resolved:
Promise.resolve(123).then((res) => {console.log(res); // 123return456; }).then((res) => {console.log(res); // 456returnPromise.resolve(123); // Notice that we are returning a Promise }).then((res) => {console.log(res); // 123 : Notice that this `then` is called with the resolved valuereturn123; })
You can aggregate the error handling of any preceding portion of the chain with a single catch:
// Create a rejected promisePromise.reject(newError('something bad happened')).then((res) => {console.log(res); // not calledreturn456; }).then((res) => {console.log(res); // not calledreturn123; }).then((res) => {console.log(res); // not calledreturn123; }).catch((err) => {console.log(err.message); // something bad happened });
The catch actually returns a new promise (effectively creating a new promise chain):
// Create a rejected promisePromise.reject(newError('something bad happened')).then((res) => {console.log(res); // not calledreturn456; }).catch((err) => {console.log(err.message); // something bad happenedreturn123; }).then((res) => {console.log(res); // 123 })
Any synchronous errors thrown in a then (or catch) result in the returned promise to fail:
Promise.resolve(123).then((res) => {thrownewError('something bad happened'); // throw a synchronous errorreturn456; }).then((res) => {console.log(res); // never calledreturnPromise.resolve(789); }).catch((err) => {console.log(err.message); // something bad happened })
Only the relevant (nearest tailing) catch is called for a given error (as the catch starts a new promise chain).
Promise.resolve(123).then((res) => {thrownewError('something bad happened'); // throw a synchronous errorreturn456; }).catch((err) => {console.log('first catch: '+err.message); // something bad happenedreturn123; }).then((res) => {console.log(res); // 123returnPromise.resolve(789); }).catch((err) => {console.log('second catch: '+err.message); // never called })
A catch is only called in case of an error in the preceding chain:
Promise.resolve(123).then((res) => {return456; }).catch((err) => {console.log("HERE"); // never called })
The fact that:
errors jump to the tailing catch (and skip any middle then calls) and
synchronous errors also get caught by any tailing catch.
effectively provides us with an async programming paradigm that allows better error handling than raw callbacks. More on this below.
TypeScript and promises
The great thing about TypeScript is that it understands the flow of values through a promise chain:
Promise.resolve(123).then((res) => {// res is inferred to be of type `number`returntrue; }).then((res) => {// res is inferred to be of type `boolean` });
Of course it also understands unwrapping any function calls that might return a promise:
functioniReturnPromiseAfter1Second():Promise<string> {returnnewPromise((resolve) => {setTimeout(() =>resolve("Hello world!"),1000); });}Promise.resolve(123).then((res) => {// res is inferred to be of type `number`returniReturnPromiseAfter1Second(); // We are returning `Promise<string>` }).then((res) => {// res is inferred to be of type `string`console.log(res); // Hello world! });
Converting a callback style function to return a promise
The most reliable way to do this is to hand write it and it doesn't have to be as verbose as the previous example e.g. converting setTimeout into a promisified delay function is super easy:
Webpack supports the util module out of the box and you can use it in the browser as well.
If you have a node callback style function as a member be sure to bind it as well to make sure it has the correct this:
constdbGet=util.promisify(db.get).bind(db);
Revisiting the JSON example
Now let's revisit our loadJSON example and rewrite an async version that uses promises. All that we need to do is read the file contents as a promise, then parse them as JSON and we are done. This is illustrated in the below example:
functionloadJSONAsync(filename:string):Promise<any> {returnreadFileAsync(filename) // Use the function we just wrote.then(function (res) {returnJSON.parse(res); });}
Usage (notice how similar it is to the original sync version introduced at the start of this section 🌹):
// good json fileloadJSONAsync('good.json').then(function (val) { console.log(val); }).catch(function (err) {console.log('good.json error',err.message); // never called })// non-existent json file.then(function () {returnloadJSONAsync('absent.json'); }).then(function (val) { console.log(val); }) // never called.catch(function (err) {console.log('absent.json error',err.message); })// invalid json file.then(function () {returnloadJSONAsync('invalid.json'); }).then(function (val) { console.log(val); }) // never called.catch(function (err) {console.log('bad.json error',err.message); });
The reason why this function was simpler is because the "loadFile(async) + JSON.parse (sync) => catch" consolidation was done by the promise chain. Also the callback was not called by us but called by the promise chain so we didn't have the chance of making the mistake of wrapping it in a try/catch.
Parallel control flow
We have seen how trivial doing a serial sequence of async tasks is with promises. It is simply a matter of chaining then calls.
However, you might potentially want to run a series of async tasks and then do something with the results of all of these tasks. Promise provides a static Promise.all function that you can use to wait for n number of promises to complete. You provide it with an array of n promises and it gives you an array of n resolved values. Below we show Chaining as well as Parallel:
// an async function to simulate loading an item from some serverfunctionloadItem(id:number):Promise<{ id:number }> {returnnewPromise((resolve) => {console.log('loading item', id);setTimeout(() => { // simulate a server delayresolve({ id: id }); },1000); });}// Chained / Sequentiallet item1, item2;loadItem(1).then((res) => { item1 = res;returnloadItem(2); }).then((res) => { item2 = res;console.log('done'); }); // overall time will be around 2s// Concurrent / ParallelPromise.all([loadItem(1),loadItem(2)]).then((res) => { [item1, item2] = res;console.log('done'); }); // overall time will be around 1s
Sometimes, you want to run a series of async tasks, but you get all you need as long as any one of these tasks is settled. Promise provides a static Promise.race function for this scenario:
var task1 =newPromise(function(resolve, reject) {setTimeout(resolve,1000,'one');});var task2 =newPromise(function(resolve, reject) {setTimeout(resolve,2000,'two');});Promise.race([task1, task2]).then(function(value) {console.log(value); // "one"// Both resolve, but task1 resolves faster});