Movatterモバイル変換


[0]ホーム

URL:


25. Promises for asynchronous programming
Table of contents
Please support this book:buy it (PDF, EPUB, MOBI) ordonate
(Ad, please don’t block.)

25.Promises for asynchronous programming

This chapter is an introduction to asynchronous programming via Promises in general and the ECMAScript 6 Promise API in particular.The previous chapter explains the foundations of asynchronous programming in JavaScript. You can consult it whenever there is something that you don’t understand in this chapter.



25.1Overview

Promises are an alternative to callbacks for delivering the results of an asynchronous computation. They require more effort from implementors of asynchronous functions, but provide several benefits for users of those functions.

The following function returns a result asynchronously, via a Promise:

functionasyncFunc(){returnnewPromise(function(resolve,reject){···resolve(result);···reject(error);});}

You callasyncFunc() as follows:

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

25.1.1Chainingthen() calls

then() always returns a Promise, which enables you to chain method calls:

asyncFunc1().then(result1=>{// Use result1returnasyncFunction2();// (A)}).then(result2=>{// (B)// Use result2}).catch(error=>{// Handle errors of asyncFunc1() and asyncFunc2()});

How the Promise P returned bythen() is settled depends on what its callback does:

Furthermore, note howcatch() handles the errors of two asynchronous function calls (asyncFunction1() andasyncFunction2()). That is, uncaught errors are passed on until there is an error handler.

25.1.2Executing asynchronous functions in parallel

If you chain asynchronous function calls viathen(), they are executed sequentially, one at a time:

asyncFunc1().then(()=>asyncFunc2());

If you don’t do that and call all of them immediately, they are basically executed in parallel (afork in Unix process terminology):

asyncFunc1();asyncFunc2();

Promise.all() enables you to be notified once all results are in (ajoin in Unix process terminology). Its input is an Array of Promises, its output a single Promise that is fulfilled with an Array of the results.

Promise.all([asyncFunc1(),asyncFunc2(),]).then(([result1,result2])=>{···}).catch(err=>{// Receives first rejection among the Promises···});

25.1.3Glossary: Promises

The Promise API is about delivering results asynchronously. APromise object (short: Promise) is a stand-in for the result, which is delivered via that object.

States:

Reacting to state changes:

Changing states: There are two operations for changing the state of a Promise. After you have invoked either one of them once, further invocations have no effect.

25.2Introduction: Promises

Promises are a pattern that helps with one particular kind of asynchronous programming: a function (or method) that returns a single result asynchronously. One popular way of receiving such a result is via a callback (“callbacks as continuations”):

asyncFunction(arg1,arg2,result=>{console.log(result);});

Promises provide a better way of working with callbacks: Now an asynchronous function returns aPromise, an object that serves as a placeholder and container for the final result. Callbacks registered via the Promise methodthen() are notified of the result:

asyncFunction(arg1,arg2).then(result=>{console.log(result);});

Compared to callbacks as continuations, Promises have the following advantages:

25.3A first example

Let’s look at a first example, to give you a taste of what working with Promises is like.

With Node.js-style callbacks, reading a file asynchronously looks like this:

fs.readFile('config.json',function(error,text){if(error){console.error('Error while reading config file');}else{try{constobj=JSON.parse(text);console.log(JSON.stringify(obj,null,4));}catch(e){console.error('Invalid JSON in file');}}});

With Promises, the same functionality is used like this:

readFilePromisified('config.json').then(function(text){// (A)constobj=JSON.parse(text);console.log(JSON.stringify(obj,null,4));}).catch(function(error){// (B)// File read error or JSON SyntaxErrorconsole.error('An error occurred',error);});

There are still callbacks, but they are provided via methods that are invoked on the result (then() andcatch()). The error callback in line B is convenient in two ways: First, it’s a single style of handling errors (versusif (error) andtry-catch in the previous example). Second, you can handle the errors of bothreadFilePromisified() and the callback in line A from a single location.

The code ofreadFilePromisified() isshown later.

25.4Three ways of understanding Promises

Let’s look at three ways of understanding Promises.

The following code contains a Promise-based functionasyncFunc() and its invocation.

functionasyncFunc(){returnnewPromise((resolve,reject)=>{// (A)setTimeout(()=>resolve('DONE'),100);// (B)});}asyncFunc().then(x=>console.log('Result: '+x));// Output:// Result: DONE

asyncFunc() returns a Promise. Once the actual result'DONE' of the asynchronous computation is ready, it is delivered viaresolve() (line B), which is a parameter of the callback that starts in line A.

So what is a Promise?

25.4.1Conceptually: calling a Promise-based function is blocking

The following code invokesasyncFunc() from the async functionmain().Async functions are a feature of ECMAScript 2017.

asyncfunctionmain(){constx=awaitasyncFunc();// (A)console.log('Result: '+x);// (B)// Same as:// asyncFunc()// .then(x => console.log('Result: '+x));}main();

The body ofmain() expresses well what’s going onconceptually, how we usually think about asynchronous computations. Namely,asyncFunc() is a blocking function call:

Prior to ECMAScript 6 and generators, you couldn’t suspend and resume code. That’s why, for Promises, you put everything that happens after the code is resumed into a callback. Invoking that callback is the same as resuming the code.

25.4.2A Promise is a container for an asynchronously delivered value

If a function returns a Promise then that Promise is like a blank into which the function will (usually) fill in its result, once it has computed it. You can simulate a simple version of this process via an Array:

functionasyncFunc(){constblank=[];setTimeout(()=>blank.push('DONE'),100);returnblank;}constblank=asyncFunc();// Wait until the value has been filled insetTimeout(()=>{constx=blank[0];// (A)console.log('Result: '+x);},200);

With Promises, you don’t access the eventual value via[0] (as in line A), you use methodthen() and a callback.

25.4.3A Promise is an event emitter

Another way to view a Promise is as an object that emits events.

functionasyncFunc(){consteventEmitter={success:[]};setTimeout(()=>{// (A)for(consthandlerofeventEmitter.success){handler('DONE');}},100);returneventEmitter;}asyncFunc().success.push(x=>console.log('Result: '+x));// (B)

Registering the event listener (line B) can be done after callingasyncFunc(), because the callback handed tosetTimeout() (line A) is executed asynchronously (after this piece of code is finished).

Normal event emitters specialize in delivering multiple events, starting as soon as you register.

In contrast, Promises specialize in delivering exactly one value and come with built-in protection against registering too late: the result of a Promise is cached and passed to event listeners that are registered after the Promise was settled.

25.5Creating and using Promises

Let’s look at how Promises are operated from the producer and the consumer side.

25.5.1Producing a Promise

As a producer, you create a Promise and send a result via it:

constp=newPromise(function(resolve,reject){// (A)···if(···){resolve(value);// success}else{reject(reason);// failure}});

25.5.2The states of Promises

Once a result was delivered via a Promise, the Promise stays locked in to that result. That means each Promise is always in either one of three (mutually exclusive) states:

A Promise issettled (the computation it represents has finished) if it is either fulfilled or rejected. A Promise can only be settled once and then stays settled. Subsequent attempts to settle have no effect.

The parameter ofnew Promise() (starting in line A) is called anexecutor:

If an exception is thrown inside the executor,p is rejected with that exception.

25.5.3Consuming a Promise

As a consumer ofpromise, you are notified of a fulfillment or a rejection viareactions – callbacks that you register with the methodsthen() andcatch():

promise.then(value=>{/* fulfillment */}).catch(error=>{/* rejection */});

What makes Promises so useful for asynchronous functions (with one-off results) is that once a Promise is settled, it doesn’t change anymore. Furthermore, there are never any race conditions, because it doesn’t matter whether you invokethen() orcatch() before or after a Promise is settled:

Note thatcatch() is simply a more convenient (and recommended) alternative to callingthen(). That is, the following two invocations are equivalent:

promise.then(null,error=>{/* rejection */});promise.catch(error=>{/* rejection */});

25.5.4Promises are always asynchronous

A Promise library has complete control over whether results are delivered to Promise reactions synchronously (right away) or asynchronously (after the current continuation, the current piece of code, is finished). However, the Promises/A+ specification demands that the latter mode of execution be always used. It states so via the followingrequirement (2.2.4) for thethen() method:

onFulfilled oronRejected must not be called until the execution context stack contains only platform code.

That means that your code can rely on run-to-completion semantics (as explained inthe previous chapter) and that chaining Promises won’t starve other tasks of processing time.

Additionally, this constraint prevents you from writing functions that sometimes return results immediately, sometimes asynchronously. This is an anti-pattern, because it makes code unpredictable. For more information, consult “Designing APIs for Asynchrony” by Isaac Z. Schlueter.

25.6Examples

Before we dig deeper into Promises, let’s use what we have learned so far in a few examples.

Some of the examples in this section are available in the GitHub repositorypromise-examples.

25.6.1Example: promisifyingfs.readFile()

The following code is a Promise-based version of the built-in Node.js functionfs.readFile().

import{readFile}from'fs';functionreadFilePromisified(filename){returnnewPromise(function(resolve,reject){readFile(filename,{encoding:'utf8'},(error,data)=>{if(error){reject(error);}else{resolve(data);}});});}

readFilePromisified() is used like this:

readFilePromisified(process.argv[2]).then(text=>{console.log(text);}).catch(error=>{console.log(error);});

25.6.2Example: promisifyingXMLHttpRequest

The following is a Promise-based function that performs an HTTP GET via the event-basedXMLHttpRequest API:

functionhttpGet(url){returnnewPromise(function(resolve,reject){constrequest=newXMLHttpRequest();request.onload=function(){if(this.status===200){// Successresolve(this.response);}else{// Something went wrong (404 etc.)reject(newError(this.statusText));}};request.onerror=function(){reject(newError('XMLHttpRequest Error: '+this.statusText));};request.open('GET',url);request.send();});}

This is how you usehttpGet():

httpGet('http://example.com/file.txt').then(function(value){console.log('Contents: '+value);},function(reason){console.error('Something went wrong',reason);});

25.6.3Example: delaying an activity

Let’s implementsetTimeout() as the Promise-based functiondelay() (similar toQ.delay()).

functiondelay(ms){returnnewPromise(function(resolve,reject){setTimeout(resolve,ms);// (A)});}// Using delay():delay(5000).then(function(){// (B)console.log('5 seconds have passed!')});

Note that in line A, we are callingresolve with zero parameters, which is the same as callingresolve(undefined). We don’t need the fulfillment value in line B, either and simply ignore it. Just being notified is enough here.

25.6.4Example: timing out a Promise

functiontimeout(ms,promise){returnnewPromise(function(resolve,reject){promise.then(resolve);setTimeout(function(){reject(newError('Timeout after '+ms+' ms'));// (A)},ms);});}

Note that the rejection after the timeout (in line A) does not cancel the request, but it does prevent the Promise being fulfilled with its result.

Usingtimeout() looks like this:

timeout(5000,httpGet('http://example.com/file.txt')).then(function(value){console.log('Contents: '+value);}).catch(function(reason){console.error('Error or timeout',reason);});

25.7Other ways of creating Promises

Now we are ready to dig deeper into the features of Promises. Let’s first explore two more ways of creating Promises.

25.7.1Promise.resolve()

Promise.resolve(x) works as follows:

That means that you can usePromise.resolve() to convert any value (Promise, thenable or other) to a Promise. In fact, it is used byPromise.all() andPromise.race() to convert Arrays of arbitrary values to Arrays of Promises.

25.7.2Promise.reject()

Promise.reject(err) returns a Promise that is rejected witherr:

constmyError=newError('Problem!');Promise.reject(myError).catch(err=>console.log(err===myError));// true

25.8Chaining Promises

In this section, we take a closer look at how Promises can be chained. The result of the method call:

P.then(onFulfilled,onRejected)

is a new Promise Q. That means that you can keep the Promise-based control flow going by invokingthen() on Q:

25.8.1Resolving Q with a normal value

If you resolve the Promise Q returned bythen() with a normal value, you can pick up that value via a subsequentthen():

asyncFunc().then(function(value1){return123;}).then(function(value2){console.log(value2);// 123});

25.8.2Resolving Q with a thenable

You can also resolve the Promise Q returned bythen() with athenable R. A thenable is any object that has a methodthen() that works likePromise.prototype.then(). Thus, Promises are thenables. Resolving with R (e.g. by returning it fromonFulfilled) means that it is inserted “after” Q: R’s settlement is forwarded to Q’sonFulfilled andonRejected callbacks. In a way, Q becomes R.

The main use for this mechanism is to flatten nestedthen() calls, like in the following example:

asyncFunc1().then(function(value1){asyncFunc2().then(function(value2){···});})

The flat version looks like this:

asyncFunc1().then(function(value1){returnasyncFunc2();}).then(function(value2){···})

25.8.3Resolving Q fromonRejected

Whatever you return in an error handler becomes a fulfillment value (not rejection value!). That allows you to specify default values that are used in case of failure:

retrieveFileName().catch(function(){// Something went wrong, use a default valuereturn'Untitled.txt';}).then(function(fileName){···});

25.8.4Rejecting Q by throwing an exception

Exceptions that are thrown in the callbacks ofthen() andcatch() are passed on to the next error handler, as rejections:

asyncFunc().then(function(value){thrownewError();}).catch(function(reason){// Handle error here});

25.8.5Chaining and errors

There can be one or morethen() method calls that don’t have error handlers. Then the error is passed on until there is an error handler.

asyncFunc1().then(asyncFunc2).then(asyncFunc3).catch(function(reason){// Something went wrong above});

25.9Common Promise chaining mistakes

25.9.1Mistake: losing the tail of a Promise chain

In the following code, a chain of two Promises is built, but only the first part of it is returned. As a consequence, the tail of the chain is lost.

// Don’t do thisfunctionfoo(){constpromise=asyncFunc();promise.then(result=>{···});returnpromise;}

This can be fixed by returning the tail of the chain:

functionfoo(){constpromise=asyncFunc();returnpromise.then(result=>{···});}

If you don’t need the variablepromise, you can simplify this code further:

functionfoo(){returnasyncFunc().then(result=>{···});}

25.9.2Mistake: nesting Promises

In the following code, the invocation ofasyncFunc2() is nested:

// Don’t do thisasyncFunc1().then(result1=>{asyncFunc2().then(result2=>{···});});

The fix is to un-nest this code by returning the second Promise from the firstthen() and handling it via a second, chained,then():

asyncFunc1().then(result1=>{returnasyncFunc2();}).then(result2=>{···});

25.9.3Mistake: creating Promises instead of chaining

In the following code, methodinsertInto() creates a new Promise for its result (line A):

// Don’t do thisclassModel{insertInto(db){returnnewPromise((resolve,reject)=>{// (A)db.insert(this.fields)// (B).then(resultCode=>{this.notifyObservers({event:'created',model:this});resolve(resultCode);// (C)}).catch(err=>{reject(err);// (D)})});}···}

If you look closely, you can see that the result Promise is mainly used to forward the fulfillment (line C) and the rejection (line D) of the asynchronous method calldb.insert() (line B).

The fix is to not create a Promise, by relying onthen() and chaining:

classModel{insertInto(db){returndb.insert(this.fields)// (A).then(resultCode=>{this.notifyObservers({event:'created',model:this});returnresultCode;// (B)});}···}

Explanations:

25.9.4Mistake: usingthen() for error handling

In principle,catch(cb) is an abbreviation forthen(null, cb). But using both parameters ofthen() at the same time can cause problems:

// Don’t do thisasyncFunc1().then(value=>{// (A)doSomething();// (B)returnasyncFunc2();// (C)},error=>{// (D)···});

The rejection callback (line D) receives all rejections ofasyncFunc1(), but it does not receive rejections created by the fulfillment callback (line A). For example, the synchronous function call in line B may throw an exception or the asynchronous function call in line C may produce a rejection.

Therefore, it is better to move the rejection callback to a chainedcatch():

asyncFunc1().then(value=>{doSomething();returnasyncFunc2();}).catch(error=>{···});

25.10Tips for error handling

25.10.1Operational errors versus programmer errors

In programs, there are two kinds of errors:

25.10.1.1Operational errors: don’t mix rejections and exceptions

For operational errors, each function should support exactly one way of signaling errors. For Promise-based functions that means not mixing rejections and exceptions, which is the same as saying that they shouldn’t throw exceptions.

25.10.1.2Programmer errors: fail quickly

For programmer errors, it can make sense to fail as quickly as possible, by throwing an exception:

functiondownloadFile(url){if(typeofurl!=='string'){thrownewError('Illegal argument: '+url);}returnnewPromise(···).}

If you do this, you must make sure that your asynchronous code can handle exceptions. I find throwing exceptions acceptable for assertions and similar things that could, in theory, be checked statically (e.g. via a linter that analyzes the source code).

25.10.2Handling exceptions in Promise-based functions

If exceptions are thrown inside the callbacks ofthen() andcatch() then that’s not a problem, because these two methods convert them to rejections.

However, things are different if you start your async function by doing something synchronous:

functionasyncFunc(){doSomethingSync();// (A)returndoSomethingAsync().then(result=>{···});}

If an exception is thrown in line A then the whole function throws an exception. There are two solutions to this problem.

25.10.2.1Solution 1: returning a rejected Promise

You can catch exceptions and return them as rejected Promises:

functionasyncFunc(){try{doSomethingSync();returndoSomethingAsync().then(result=>{···});}catch(err){returnPromise.reject(err);}}
25.10.2.2Solution 2: executing the sync code inside a callback

You can also start a chain ofthen() method calls viaPromise.resolve() and execute the synchronous code inside a callback:

functionasyncFunc(){returnPromise.resolve().then(()=>{doSomethingSync();returndoSomethingAsync();}).then(result=>{···});}

An alternative is to start the Promise chain via the Promise constructor:

functionasyncFunc(){returnnewPromise((resolve,reject)=>{doSomethingSync();resolve(doSomethingAsync());}).then(result=>{···});}

This approach saves you a tick (the synchronous code is executed right away), but it makes your code less regular.

25.10.3Further reading

Sources of this section:

25.11Composing Promises

Composing means creating new things out of existing pieces. We have already encountered sequential composition of Promises: Given two Promises P and Q, the following code produces a new Promise that executes Q after P is fulfilled.

P.then(()=>Q)

Note that this is similar to the semicolon for synchronous code: Sequential composition of the synchronous operationsf() andg() looks as follows.

f();g()

This section describes additional ways of composing Promises.

25.11.1Manually forking and joining computations

Let’s assume you want to perform two asynchronous computations,asyncFunc1() andasyncFunc2() in parallel:

// Don’t do thisasyncFunc1().then(result1=>{handleSuccess({result1});});.catch(handleError);asyncFunc2().then(result2=>{handleSuccess({result2});}).catch(handleError);constresults={};functionhandleSuccess(props){Object.assign(results,props);if(Object.keys(results).length===2){const{result1,result2}=results;···}}leterrorCounter=0;functionhandleError(err){errorCounter++;if(errorCounter===1){// One error means that everything failed,// only react to first error···}}

The two function callsasyncFunc1() andasyncFunc2() are made withoutthen() chaining. As a consequence, they are both executed immediately and more or less in parallel. Execution is now forked; each function call spawned a separate “thread”. Once both threads are finished (with a result or an error), execution is joined into a single thread in eitherhandleSuccess() orhandleError().

The problem with this approach is that it involves too much manual and error-prone work. The fix is to not do this yourself, by relying on the built-in methodPromise.all().

25.11.2Forking and joining computations viaPromise.all()

Promise.all(iterable) takes an iterable over Promises (thenables and other values are converted to Promises viaPromise.resolve()). Once all of them are fulfilled, it fulfills with an Array of their values. Ifiterable is empty, the Promise returned byall() is fulfilled immediately.

Promise.all([asyncFunc1(),asyncFunc2(),]).then(([result1,result2])=>{···}).catch(err=>{// Receives first rejection among the Promises···});

25.11.3map() viaPromise.all()

One nice thing about Promises is that many synchronous tools still work, because Promise-based functions return results. For example, you can use the Array methodmap():

constfileUrls=['http://example.com/file1.txt','http://example.com/file2.txt',];constpromisedTexts=fileUrls.map(httpGet);

promisedTexts is an Array of Promises. We can usePromise.all(), which we have already encountered in the previous section, to convert that Array to a Promise that fulfills with an Array of results.

Promise.all(promisedTexts).then(texts=>{for(consttextoftexts){console.log(text);}}).catch(reason=>{// Receives first rejection among the Promises});

25.11.4Timing out viaPromise.race()

Promise.race(iterable) takes an iterable over Promises (thenables and other values are converted to Promises viaPromise.resolve()) and returns a Promise P. The first of the input Promises that is settled passes its settlement on to the output Promise. Ifiterable is empty then the Promise returned byrace() is never settled.

As an example, let’s usePromise.race() to implement a timeout:

Promise.race([httpGet('http://example.com/file.txt'),delay(5000).then(function(){thrownewError('Timed out')});]).then(function(text){···}).catch(function(reason){···});

25.12Two useful additional Promise methods

This section describes two useful methods for Promises that many Promise libraries provide. They are only shown to further demonstrate Promises, you should not add them toPromise.prototype (this kind of patching should only be done by polyfills).

25.12.1done()

When you chain several Promise method calls, you risk silently discarding errors. For example:

functiondoSomething(){asyncFunc().then(f1).catch(r1).then(f2);// (A)}

Ifthen() in line A produces a rejection, it will never be handled anywhere. The Promise library Q provides a methoddone(), to be used as the last element in a chain of method calls. It either replaces the lastthen() (and has one to two arguments):

functiondoSomething(){asyncFunc().then(f1).catch(r1).done(f2);}

Or it is inserted after the lastthen() (and has zero arguments):

functiondoSomething(){asyncFunc().then(f1).catch(r1).then(f2).done();}

Quoting theQ documentation:

The Golden Rule ofdone versusthen usage is: either return your promise to someone else, or if the chain ends with you, calldone to terminate it. Terminating withcatch is not sufficient because the catch handler may itself throw an error.

This is how you would implementdone() in ECMAScript 6:

Promise.prototype.done=function(onFulfilled,onRejected){this.then(onFulfilled,onRejected).catch(function(reason){// Throw an exception globallysetTimeout(()=>{throwreason},0);});};

Whiledone’s functionality is clearly useful, it has not been added to ECMAScript 6. The idea was to first explore how much engines can detect automatically. Depending on how well that works, it may to be necessary to introducedone().

25.12.2finally()

Sometimes you want to perform an action independently of whether an error happened or not. For example, to clean up after you are done with a resource. That’s what the Promise methodfinally() is for, which works much like thefinally clause in exception handling. Its callback receives no arguments, but is notified of either a resolution or a rejection.

createResource(···).then(function(value1){// Use resource}).then(function(value2){// Use resource}).finally(function(){// Clean up});

This is howDomenic Denicolaproposes to implementfinally():

Promise.prototype.finally=function(callback){constP=this.constructor;// We don’t invoke the callback in here,// because we want then() to handle its exceptionsreturnthis.then(// Callback fulfills => continue with receiver’s fulfillment or rejec\tion// Callback rejects => pass on that rejection (then() has no 2nd para\meter!)value=>P.resolve(callback()).then(()=>value),reason=>P.resolve(callback()).then(()=>{throwreason}));};

The callback determines how the settlement of the receiver (this) is handled:

Example 1 (byJake Archibald): usingfinally() to hide a spinner. Simplified version:

showSpinner();fetchGalleryData().then(data=>updateGallery(data)).catch(showNoDataError).finally(hideSpinner);

Example 2 (byKris Kowal): usingfinally() to tear down a test.

constHTTP=require("q-io/http");constserver=HTTP.Server(app);returnserver.listen(0).then(function(){// run test}).finally(server.stop);

25.13Node.js: using callback-based sync functions with Promises

The Promise library Q hastool functions for interfacing with Node.js-style(err, result) callback APIs. For example,denodeify converts a callback-based function to a Promise-based one:

constreadFile=Q.denodeify(FS.readFile);readFile('foo.txt','utf-8').then(function(text){···});

denodify is a micro-library that only provides the functionality ofQ.denodeify() and complies with the ECMAScript 6 Promise API.

25.14ES6-compatible Promise libraries

There are many Promise libraries out there. The following ones conform to the ECMAScript 6 API, which means that you can use them now and easily migrate to native ES6 later.

Minimal polyfills:

Larger Promise libraries:

ES6 standard library polyfills:

25.15Next step: using Promises via generators

Implementing asynchronous functions via Promises is more convenient than via events or callbacks, but it’s still not ideal:

The solution is to bring blocking calls to JavaScript. Generators let us do that, via libraries: In the following code, I usethe control flow library co to asynchronously retrieve two JSON files.

co(function*(){try{const[croftStr,bondStr]=yieldPromise.all([// (A)getFile('http://localhost:8000/croft.json'),getFile('http://localhost:8000/bond.json'),]);constcroftJson=JSON.parse(croftStr);constbondJson=JSON.parse(bondStr);console.log(croftJson);console.log(bondJson);}catch(e){console.log('Failure to read: '+e);}});

In line A, execution blocks (waits) viayield until the result ofPromise.all() is ready. That means that the code looks synchronous while performing asynchronous operations.

Details are explained inthe chapter on generators.

25.16Promises in depth: a simple implementation

In this section, we will approach Promises from a different angle: Instead of learning how to use the API, we will look at a simple implementation of it. This different angle helped me greatly with making sense of Promises.

The Promise implementation is calledDemoPromise. In order to be easier to understand, it doesn’t completely match the API. But it is close enough to still give you much insight into the challenges that actual implementations face.

DemoPromise is available on GitHub, in the repositorydemo_promise.

DemoPromise is a class with three prototype methods:

That is,resolve andreject are methods (versus functions handed to a callback parameter of the constructor).

25.16.1A stand-alone Promise

Our first implementation is a stand-alone Promise with minimal functionality:

This is how this first implementation is used:

constdp=newDemoPromise();dp.resolve('abc');dp.then(function(value){console.log(value);// abc});

The following diagram illustrates how our firstDemoPromise works:

25.16.1.1DemoPromise.prototype.then()

Let’s examinethen() first. It has to handle two cases:

then(onFulfilled,onRejected){constself=this;constfulfilledTask=function(){onFulfilled(self.promiseResult);};constrejectedTask=function(){onRejected(self.promiseResult);};switch(this.promiseState){case'pending':this.fulfillReactions.push(fulfilledTask);this.rejectReactions.push(rejectedTask);break;case'fulfilled':addToTaskQueue(fulfilledTask);break;case'rejected':addToTaskQueue(rejectedTask);break;}}

The previous code snippet uses the following helper function:

functionaddToTaskQueue(task){setTimeout(task,0);}
25.16.1.2DemoPromise.prototype.resolve()

resolve() works as follows: If the Promise is already settled, it does nothing (ensuring that a Promise can only be settled once). Otherwise, the state of the Promise changes to'fulfilled' and the result is cached inthis.promiseResult. Next, all fulfillment reactions, that have been enqueued so far, are be triggered.

resolve(value){if(this.promiseState!=='pending')return;this.promiseState='fulfilled';this.promiseResult=value;this._clearAndEnqueueReactions(this.fulfillReactions);returnthis;// enable chaining}_clearAndEnqueueReactions(reactions){this.fulfillReactions=undefined;this.rejectReactions=undefined;reactions.map(addToTaskQueue);}

reject() is similar toresolve().

25.16.2Chaining

The next feature we implement is chaining:

Obviously, onlythen() changes:

then(onFulfilled,onRejected){constreturnValue=newPromise();// (A)constself=this;letfulfilledTask;if(typeofonFulfilled==='function'){fulfilledTask=function(){constr=onFulfilled(self.promiseResult);returnValue.resolve(r);// (B)};}else{fulfilledTask=function(){returnValue.resolve(self.promiseResult);// (C)};}letrejectedTask;if(typeofonRejected==='function'){rejectedTask=function(){constr=onRejected(self.promiseResult);returnValue.resolve(r);// (D)};}else{rejectedTask=function(){// `onRejected` has not been provided// => we must pass on the rejectionreturnValue.reject(self.promiseResult);// (E)};}···returnreturnValue;// (F)}

then() creates and returns a new Promise (lines A and F). Additionally,fulfilledTask andrejectedTask are set up differently: After a settlement…

25.16.3Flattening

Flattening is mostly about making chaining more convenient: Normally, returning a value from a reaction passes it on to the nextthen(). If we return a Promise, it would be nice if it could be “unwrapped” for us, like in the following example:

asyncFunc1().then(function(value1){returnasyncFunc2();// (A)}).then(function(value2){// value2 is fulfillment value of asyncFunc2() Promiseconsole.log(value2);});

We returned a Promise in line A and didn’t have to nest a call tothen() inside the current method, we could invokethen() on the method’s result. Thus: no nestedthen(), everything remains flat.

We implement this by letting theresolve() method do the flattening:

We can make flattening more generic if we allow Q to be a thenable (instead of only a Promise).

To implement locking-in, we introduce a new boolean flagthis.alreadyResolved. Once it is true,this is locked and can’t be resolved anymore. Note thatthis may still be pending, because its state is now the same as the Promise it is locked in on.

resolve(value){if(this.alreadyResolved)return;this.alreadyResolved=true;this._doResolve(value);returnthis;// enable chaining}

The actual resolution now happens in the private method_doResolve():

_doResolve(value){constself=this;// Is `value` a thenable?if(typeofvalue==='object'&&value!==null&&'then'invalue){// Forward fulfillments and rejections from `value` to `this`.// Added as a task (versus done immediately) to preserve async semant\ics.addToTaskQueue(function(){// (A)value.then(functiononFulfilled(result){self._doResolve(result);},functiononRejected(error){self._doReject(error);});});}else{this.promiseState='fulfilled';this.promiseResult=value;this._clearAndEnqueueReactions(this.fulfillReactions);}}

The flattening is performed in line A: Ifvalue is fulfilled, we wantself to be fulfilled and ifvalue is rejected, we wantself to be rejected. The forwarding happens via the private methods_doResolve and_doReject, to get around the protection viaalreadyResolved.

25.16.4Promise states in more detail

With chaining, the states of Promises become more complex (as covered bySect. 25.4 of the ECMAScript 6 specification):

If you are onlyusing Promises, you can normally adopt a simplified worldview and ignore locking-in. The most important state-related concept remains “settledness”: a Promise is settled if it is either fulfilled or rejected. After a Promise is settled, it doesn’t change, anymore (state and fulfillment or rejection value).

If you want toimplement Promises then “resolving” matters, too and is now harder to understand:

25.16.5Exceptions

As our final feature, we’d like our Promises to handle exceptions in user code as rejections. For now, “user code” means the two callback parameters ofthen().

The following excerpt shows how we turn exceptions insideonFulfilled into rejections – by wrapping atry-catch around its invocation in line A.

then(onFulfilled,onRejected){···letfulfilledTask;if(typeofonFulfilled==='function'){fulfilledTask=function(){try{constr=onFulfilled(self.promiseResult);// (A)returnValue.resolve(r);}catch(e){returnValue.reject(e);}};}else{fulfilledTask=function(){returnValue.resolve(self.promiseResult);};}···}

25.16.6Revealing constructor pattern

If we wanted to turnDemoPromise into an actual Promise implementation, we’d still need to implementthe revealing constructor pattern [2]: ES6 Promises are not resolved and rejected via methods, but via functions that are handed to theexecutor, the callback parameter of the constructor.

If the executor throws an exception then “its” Promise must be rejected.

25.17Advantages and limitations of Promises

25.17.1Advantages of Promises

25.17.1.1Unifying asynchronous APIs

One important advantage of Promises is that they will increasingly be used by asynchronous browser APIs and unify currently diverse and incompatible patterns and conventions. Let’s look at two upcoming Promise-based APIs.

The fetch API is a Promise-based alternative to XMLHttpRequest:

fetch(url).then(request=>request.text()).then(str=>···)

fetch() returns a Promise for the actual request,text() returns a Promise for the content as a string.

TheECMAScript 6 API for programmatically importing modules is based on Promises, too:

System.import('some_module.js').then(some_module=>{···})
25.17.1.2Promises versus events

Compared to events, Promises are better for handling one-off results. It doesn’t matter whether you register for a result before or after it has been computed, you will get it. This advantage of Promises is fundamental in nature. On the flip side, you can’t use them for handling recurring events. Chaining is another advantage of Promises, but one that could be added to event handling.

25.17.1.3Promises versus callbacks

Compared to callbacks, Promises have cleaner function (or method) signatures. With callbacks, parameters are used for input and output:

fs.readFile(name,opts?,(err,string|Buffer)=>void)

With Promises, all parameters are used for input:

readFilePromisified(name,opts?):Promise<string|Buffer>

Additional Promise advantages include:

25.17.2Promises are not always the best choice

Promises work well for for single asynchronous results. They are not suited for:

ECMAScript 6 Promises lack two features that are sometimes useful:

The Q Promise library hassupport for the latter and there areplans to add both capabilities to Promises/A+.

25.18Reference: the ECMAScript 6 Promise API

This section gives an overview of the ECMAScript 6 Promise API, as described in thespecification.

25.18.1Promise constructor

The constructor for Promises is invoked as follows:

constp=newPromise(function(resolve,reject){···});

The callback of this constructor is called anexecutor. The executor can use its parameters to resolve or reject the new Promisep:

25.18.2StaticPromise methods

25.18.2.1Creating Promises

The following two static methods create new instances of their receivers:

25.18.2.2Composing Promises

Intuitively, the static methodsPromise.all() andPromise.race() compose iterables of Promises to a single Promise. That is:

The methods are:

25.18.3Promise.prototype methods

25.18.3.1Promise.prototype.then(onFulfilled, onRejected)

Default values for omitted reactions could be implemented like this:

functiondefaultOnFulfilled(x){returnx;}functiondefaultOnRejected(e){throwe;}
25.18.3.2Promise.prototype.catch(onRejected)

25.19Further reading

[1] “Promises/A+”, edited by Brian Cavalier and Domenic Denicola (the de-facto standard for JavaScript Promises)

[2] “The Revealing Constructor Pattern” by Domenic Denicola (this pattern is used by thePromise constructor)

Next:VI Miscellaneous

[8]ページ先頭

©2009-2025 Movatter.jp