|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- title: Promises: All The Wrong Ways
- url: https://blog.getify.com/promises-wrong-ways/
- hash_url: 2a774113a3de3e53ca5bf9d45d0b31d1
-
- <p>If you’re not well-versed on JavaScript Promises yet, I urge you to <a href="https://blog.getify.com/promises-part-1/">read up</a> on <a href="https://github.com/getify/You-Dont-Know-JS/blob/master/async%20&%20performance/ch3.md">them</a> before diving into this article.</p>
- <p>Promises are the <em>promise</em> of a vastly improved asynchrony over our previous primitives (notably: callbacks). By abstracting a potentially future value so that it can be treated as time-independent — we don’t have to care if the value is still pending or already finalized — we remove <strong>time</strong> from our concerns, making our programs far easier to reason about.</p>
- <p>But I’m not writing to convince you to use promises. Nor am I writing to convince you <em>not</em> to use them. I simply want to call out some ways promises are being used in the wild that I think miss the point, muddy up your code, and dilute the benefits of using them in the first place.</p>
- <h3 id="false-start">False Start</h3>
- <p>You use some lib that has a function called <code>foo()</code>, and supposedly when you call it, you get a promise back. But… exactly what flavor of promise is it? Your app uses Bluebird promises because you love those extra API methods, but do you know what kind of promise this other <code>foo()</code> function is going to give you? Are you even sure it’s a reliably Promises/A+ compliant promise?</p>
- <p>How much can you really trust that the promise you’re getting is the promise you think you’re getting? And what if you are using different libraries that each have their own flavor of promises returned? How are you managing the mixing of all these promises in your code?</p>
- <p>The plain fact, which is inconvenient and troubling, is this: this code is a bad anti-pattern:</p>
- <pre class="code">
- foo()
- .then( nextStep )
- ..
- </pre>
- <p>You need to normalize the promise you’re receiving back to make sure it’s a legitimate promise and of the flavor you assume.</p>
- <p>Fortunately, this is relatively easy, if not a teeny bit tedious. Use <code>Promise.resolve(..)</code> — or whatever the equivalent is for your promise library of choice — to normalize the returned value into a promise you recognize and trust:</p>
- <pre class="code">
- Promise.resolve( foo() )
- .then( nextStep )
- ..
- </pre>
- <p>If <code>foo()</code> returns a promise that is the same as you are expecting, it’ll pass through <code>Promise.resolve(..)</code> untouched. Otherwise, it’ll be adapted so that the return of <code>Promise.resolve(..)</code> <em>is</em> a promise you recognize and trust.</p>
- <p>But wait… does that mean we have to wrap every single promise we get? Eh, not exactly.</p>
- <p>The <code>then(..)</code> method automatically does this same sort of normalizing on any return value it receives back from either the fulfillment or rejection handlers.</p>
- <pre class="code">
- Promise.resolve( foo() )
- .then( bar )
- .then( lastStep );
- </pre>
- <p>Even if <code>bar()</code> produces a different kind of promise, that promise is subsumed by <code>then(..)</code> — the <code>then(..)</code> that comes from the kind of promise we want, via <code>Promise.resolve(..)</code> — meaning that the normalization is automatically implied as we’d want.</p>
- <p>So it’s really only the beginning of the promise chain that we need to be more careful with. We need to make sure to normalize all first-of-chain promises, just in case.</p>
- <h4 id="start-delays">Start Delays</h4>
- <p>Speaking of the start of a promise chain, another mistake that’s often made is adding unnecessary promise resolution steps. For example:</p>
- <pre class="code">
- Promise.resolve()
- .then( firstStep );
- </pre>
- <p>Why would we do this? The first reason is that it automatically normalizes <code>firstStep()</code>‘s returned value/promise, as just described. The second we’ll cover in the next section.</p>
- <p>But instead of calling <code>firstStep()</code> right away, here it’ll be delayed until the end of the event-loop tick. Is that really a big deal? Usually not, but when you do this over and over again, those delays <em>can</em> add up.</p>
- <p>Don’t put an extra empty tick at the beginning of your promise chain. Just start the chain right away.</p>
- <p>And while we’re discussing it, don’t do this either:</p>
- <pre class="code">
- new Promise( function(resolve,reject){
- firstStep()
- .then( resolve, reject );
- } )
- ..
- </pre>
- <p>You may think that’d be obnoxious and obviously bad, but it’s more common than you’d realize and really shouldn’t be done.</p>
- <h4 id="failing-to-start">Failing To Start</h4>
- <p>What if there’s any case where <code>firstStep()</code> being called could result directly in a synchronous exception — not a rejected promise?</p>
- <p>Most people think such method behavior shouldn’t ever be designed — indeed, many would call such a function an anti-pattern in and of itself — but this post isn’t about bad patterns with API design, it’s about bad practices with using the promises you get.</p>
- <p>The fact is, some functions of this sort do exist. If you have to deal with one, you’re probably going to want trap such an exception and turn it into a promise rejection that can be dealt with in the normalized promise chain.</p>
- <p>You might consider doing something manual like this:</p>
- <pre class="code">
- var p;
-
- try {
- p = Promise.resolve( firstStep() );
- }
- catch (err) {
- p = Promise.reject( err );
- }
-
- p
- .then( .. )
- ..
- </pre>
- <p>We not only normalize the return value to the desired promise flavor with <code>Promise.resolve(..)</code>, but the <code>try..catch</code> also normalizes any caught exception into the rejection. That’s effective, but ugly, and this code’s verbosity doesn’t actually contribute to easier understanding, IMO.</p>
- <p>This concern is where two earlier-mentioned patterns probably originated from:</p>
- <pre class="code">
- Promise.resolve()
- .then( firstStep )
- ..
-
- // and:
-
- new Promise( function(resolve,reject){
- firstStep()
- .then( resolve, reject );
- } )
- ..
- </pre>
- <p>In both cases, if <code>firstStep()</code> results in an exception, it will automatically be caught by the Promise machinery, and turned into a promise rejection that the rest of the chain can articulate handling for.</p>
- <p>But I think a cleaner way of doing this is to use an <a href="https://blog.getify.com/not-awaiting-1/"><code>async function</code></a>, specifically in its <code>=></code> arrow-function IIFE form:</p>
- <pre class="code">
- async _=>firstStep()()
- .then( .. )
- ..
- </pre>
- <p><strong>Note:</strong> <code>async function</code>s are a proposed addition to the JS standard, but have not officially landed. They are experimentally implemented in several major browsers, but as with all recent JS changes, you’ll likely be transpiling them, which is fully supported by Babel and others.</p>
- <p>The <code>async function</code> normalizes whatever comes back from the <code>firstStep()</code> function call into a promise, including catching any exception and turning it into a promise rejection.</p>
- <p>The small caveat here, different from using <code>Promise.resolve(..)</code> manually, is that the promise coming back from an <code>async function</code> is going to be a built-in native Promise, not some custom extended flavor. Use an additional <code>Promise.resolve(..)</code> or equivalent to cast it into a promise you’re happy with.</p>
- <h3 id="deconstruction">Deconstruction</h3>
- <p>When you construct a promise manually, the initialization function you pass to the constructor receives the capabilities for resolving that promise:</p>
- <pre class="code">
- var pr = new Promise( function init(resolve,reject){
- // call `resolve()` to fulfill the promise
- // or call `reject()` to reject it
- } );
- </pre>
- <p>By convention, we usually call those functions <code>resolve()</code> and <code>reject()</code>.</p>
- <p>Back before the standard for promises was finalized, there was a notion of these two capabilities — referred to collectively at the time as the <strong>deferred</strong> — being separately created from the promise they controlled.</p>
- <p>Imagine creating a promise more like this:</p>
- <pre class="code">
- var [ pr, def ] = new Promise();
-
- // call `def.resolve()` to fulfill the promise
- // or call `def.reject()` to reject it
- </pre>
- <p><strong>Note:</strong> FYI: <code>[pr,def] = ..</code> is utilizing the new <a href="https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20&%20beyond/ch2.md#destructuring">ES6 destructuring syntax</a>, specifically <strong>array destructuring</strong>.</p>
- <p>Having the promise and the deferred separated can seem quite useful in certain cases if the code that is doing this construction needs to transfer the resolution capabilities (deferred) to one part of the code and the observation capabilities (the promise) to another.</p>
- <p>For example:</p>
- <pre class="code">
- var [ pr, def ] = new Promise();
-
- // later:
-
- setupEvent( def.resolve, def.reject );
-
- // even later:
-
- handleEvent( pr );
- </pre>
- <p>Essentially, this approach treates a promise like a single-value event conduit. One part of the app holds the “write end” of the pipe, the <strong>deferred</strong>. Another part of the app holds the “read end”, the promise.</p>
- <p>Though the Promise API doesn’t work like this, it is possible to emulate, by <strong>extracting the resolution capabilities</strong>:</p>
- <pre class="code">
- function makePromise() {
- var def = {};
- var pr = new Promise( function init(resolve,reject){
- def.resolve = resolve;
- def.reject = reject;
- } );
- return [ pr, def ];
- }
-
- var [ pr, def ] = makePromise();
- </pre>
- <p>As you can see, we extract the <code>resolve()</code> and <code>reject()</code> capabilities from the promise initialization function storing them on <code>def</code>, then return both <code>pr</code> and <code>def</code> to the calling code.</p>
- <p>Undeniably, there are cases where this is quite convenient. I’ve done this many times myself.</p>
- <p>But I will assert that it’s an anti-pattern and that you should try to avoid it. At a minimum, if you have to do so, you should be hiding that kind of trick inside the plumbing of a library, not exposing the deferred and promise separately at the top level of logic in your application.</p>
- <p>If you agree with me that promise resolution extraction smells a little hackish, it’s because IMO we’re abusing a promise to act like a different kind of abstraction (that is already more well understood).</p>
- <h4 id="abstractions-for-value-communication">Abstractions For Value Communication</h4>
- <p>One way to envision this alternate, more well-suited abstraction is as an Event Emitter:</p>
- <pre class="code">
- var evt = new EventEmitter();
-
- // later:
-
- setupEvent( evt );
-
- // even later:
-
- handleEvent( evt );
- </pre>
- <p>Inside <code>setupEvent(..)</code>, we use <code>evt</code> to call <code>evt.emit(..)</code> to set an event of some name, and optionally pass along any data with that event. Inside <code>handleEvent(..)</code>, we call something like <code>evt.once(..)</code> to set up a single-occurring event listener for that pre-agreed event name.</p>
- <p>Either with promises (as shown earlier) or these Event Emitter events, we use a <strong>push</strong> model for communicating the value at the appropriate future time.</p>
- <p>But another way to abstract this problem is with a <strong>pull</strong> model. In other words, instead of the producer pushing the value once it’s ready, the consumer pulls the value when it’s ready to handle it.</p>
- <p>In my <a href="https://github.com/getify/asynquence">asynquence</a> library, I have an abstraction called “iterable sequences”, which is basically like an queue. You can add new handlers to the queue by calling <code>then(..)</code>, and then iterate through the queue using the standard <code>next()</code> iterator interface.</p>
- <p>For example:</p>
- <pre class="code">
- var isq = ASQ.iterable();
-
- isq
- .then( function(val){
- return val * 2;
- } )
- .then( function(val){
- return !!val;
- } )
- .then( function(val){
- return val.toUpperCase();
- } );
-
- isq.next( 21 ); // { value: 42, done: false }
- isq.next( false ); // { value: true, done: false }
- isq.next( "hello" ); // { value: "HELLO", done: false }
- isq.next(); // { done: true }
- </pre>
- <p>An <em>iterable sequence</em> is a different kind of abstraction that encapsulates both the resolution and observation capabilities into a single interface. We pass around that single abstraction wrapper instead of splitting up the promise and the deferred.</p>
- <p><strong>Note:</strong> By the way, that’s similar in nature to <a href="http://reactivex.io/documentation/subject.html"><em>Observable Subject</em>s</a>.</p>
- <p>Aside from custom library abstractions, we can use already built-in parts of JavaScript to create a <strong>pull</strong> abstraction directly. A generator can create a producer iterator:</p>
- <pre class="code">
- function *setupProducer() {
- var pr = new Promise(function(resolve,reject){
- setupEvent( resolve, reject );
- });
- yield pr;
- }
-
- var producer = setupProducer();
- </pre>
- <p>What’s going on here? We want an iterator that when we call <code>next()</code> on it, we’ll get back a value. But when we pull it, that value might not be ready yet. So instead, we yield a promise for that single next value that the producer will eventually produce. So the consumer is <strong>pulling</strong> (iterating) out each <strong>push</strong> instance (promise).</p>
- <p>Why is that different from the earlier promise capability extraction I called an anti-pattern? Because we’re not tracking the promise and its paired deferred separately at the top level of logic; we’ve hidden those as implementation details inside the <code>setupProducer()</code> generator.</p>
- <p>What we instead pass around is the wrapper around this capability: <code>producer</code>, which is an iterator. That’s a better abstraction that makes code easier to reason about. Tracking promises and their deferred capabilities separately is much harder to reason about in complex applications.</p>
- <p><strong>Note:</strong> The other advantage, though hard to see in this snippet, is that our generator/iterator could produce a new promise for each pull request, so we can use this conduit for communicating multiple values, instead of just once when there’s only a single promise involved. We get a new promise for each value.</p>
- <p>Here’s how we later <strong>pull</strong> a value:</p>
- <pre class="code">
- var pr = producer.next();
-
- pr.then( function(value){
- // ..
- } );
- </pre>
- <p>We call <code>next()</code> on our <code>producer</code> iterator, and we get out a promise, that we can then listen to.</p>
- <p>By the way, this notion of a generator that produces promises is not my own invention. There’s currently <a href="https://github.com/tc39/proposal-async-iteration#asynchronous-iterators-for-ecmascript">a proposal to add a new primitive</a> to JavaScript called an “async generator”. It basically does what I just sketched out: it gives an iterator that returns a new promise each time it is <code>next()</code> iterated.</p>
- <p>These various abstractions are far better suited for modeling the transmission of a value asynchronously from one part of the application to another. Promises have to be twisted and contorted to serve that purpose.</p>
- <p>Don’t do it just because you <em>can</em> do it. Promises shouldn’t be used that way, at least not visibly to your main application code.</p>
- <h3 id="promise-side-effects">Promise Side-Effects</h3>
- <p>Promises are, at their most core, a model for values. The fact that we use functions with the Promise API as the vehicle for unwrapping those values is an unfortunate temptation trap we can too easily fall into.</p>
- <p>Functional programming practices implore us to rely on value immutability for easier to reason about code, as well as using functions without side-effects (aka, pure). But it’s all too common that promises are used in very non-FP-friendly ways. Specifically, it’s far too common to either create or rely on side-effects with the functions passed to promise methods.</p>
- <p>The first kind of common side-effect is relative sequencing between multiple promises.</p>
- <p>Consider:</p>
- <pre class="code">
- firstStep()
- .then( secondStep )
- .then( function(){
- console.log( "two" );
- } );
-
- firstStep()
- .then( function(){
- console.log( "one" );
- } );
- </pre>
- <p>What order do you think those two console messages are printed? Almost certainly in “one”, “two” order, right? Almost. It’s actually possible that the second invocation of the <code>firstStep()</code> function will produce a promise that isn’t resolved until after the <code>secondStep()</code> promise is finished, resulting in “two”, “one” order.</p>
- <p>If you author code like this, what you’re saying is, “I believe I can predict the relative ordering of asynchronous activity, and rely on that for my application.” Unfortunately, you’re the only one; noone else who reads your code will be able to do so to the same effective extent. ð</p>
- <p>Another example:</p>
- <pre class="code">
- Promise.resolve().then( function(){
- console.log( "two" );
- } );
-
- console.log( "one" );
- </pre>
- <p>The console here will always print “one”, “two”, because we’re taking advantage of our knowledge that a <code>then(..)</code> handler will always be run asynchronously.</p>
- <p>By “run asynchronously”, we mean strictly after the current synchronous execution context, but we don’t actually mean on the next tick of the event loop, like with a <code>setTimeout(..0)</code> type of asynchrony:</p>
- <pre class="code">
- setTimeout( function(){
- console.log( "three" );
- }, 0 );
-
- Promise.resolve().then( function(){
- console.log( "two" );
- } );
-
- console.log( "one" );
- </pre>
- <p>In a conforming/native implementation, this snippet will always print “one”, “two”, “three”, because the “two” console message function is on the microtask queue to be run at the end of the snippet (after the “one” console message), but the <code>setTimeout(..0)</code> schedules the “three” message on the next event loop tick.</p>
- <p>As intrictate as this situation is, I’ve seen plenty of code that <strong>relies on</strong> the relative sequencing of these types of actions, even with multiple promises:</p>
- <pre class="code">
- Promise.resolve().then( function(){
- console.log( "one" );
- } );
-
- Promise.resolve().then( function(){
- console.log( "two" );
- } );
- </pre>
- <p>The specification doesn’t explicitly require “one”, “two” sequencing here. However, in practice, it’ll probably happen that way, because there will probably only be one internal microtask queue that those two separate <code>then(..)</code> actions are scheduled onto, in first-come-first-served order.</p>
- <p>While you might be able to rely on this as an implementation detail, I think these kinds of tricks are an absolutely terrible idea.</p>
- <p>If there’s some dependency between the actions, then express it directly — do <strong>not</strong> rely on perceived behaviors or async microtask processing. Even if the code “works”, you’ve written <strong>entirely un-reason-able code</strong>, as future readers will almost certainly not be able to intuit those same implied sequencing relationships.</p>
- <p>Inter-promise sequencing may or may not be guaranteed in any given scenario, but one thing that <em>is</em> guaranteed is that relying on this behavior makes your code worse. Don’t do it!</p>
- <h4 id="construction-sequencing">Construction Sequencing</h4>
- <p>Another place where this sequencing side-effects thing is often seen is with the <code>Promise(..)</code> constructor.</p>
- <p>The idea is to take advantage of the knowledge that the initialization function you pass in is <strong>synchronously</strong> executed, even though the other parts of the Promise API (like the handlers you pass to <code>then(..)</code>) execute those functions <strong>asynchronously</strong>.</p>
- <p>We already saw this illustrated earlier when we discussed the capability extraction.</p>
- <p>For example:</p>
- <pre class="code">
- var fn;
-
- var pr = new Promise( function init(resolve){
- fn = resolve;
- } );
-
- fn( 42 );
- </pre>
- <p>We <em>know</em> we can call <code>fn()</code> right away because we know the <code>init(..)</code> function is executed synchronously by the Promise constructor.</p>
- <p>I’m asserting that this is a <strong>bad idea</strong> to exploit this sort of semantic in your normal application code alongside other <code>then(..)</code> function calls, since those are always asynchronous. Having both sync and async behaviors mixed together makes the code harder to understand.</p>
- <p>The above snippet is equivalent to just:</p>
- <pre class="code">
- var pr = Promise.resolve( 42 );
- </pre>
- <p>I’m not saying to avoid the Promise constructor. But don’t use it as a synchronous side-effect producing function — specifically, relying on the sequencing of the side effects — if you can at all avoid it.</p>
- <h4 id="scopes">Scopes</h4>
- <p>Another pattern that seems to crop up is the sharing of values across scopes via side-effects, to get around the fact that promises only model/fulfill single values.</p>
- <p>Consider:</p>
- <pre class="code">
- function getOrderDetails(orderID) {
- var _order;
-
- return db.find( "orders", orderID )
- .then( function(order){
- _order = order;
- return db.find( "customers", order.customerID )
- } )
- .then( function(customer){
- _order.customer = customer;
- return _order;
- } );
- }
- </pre>
- <p>What’s going on here? We first make a <code>db.find("orders" ..)</code> call, which gives us the <code>order</code>. We not only need that to get <code>order.customerID</code> for the <code>db.find("customers" ..)</code> call, but we’ll also need <code>order</code> for the final result of our <code>getOrderDetails(..)</code> operation.</p>
- <p>But the second <code>then(..)</code> will receive the result of the <code>db.find("customers" ..)</code> call, which will not pass along the <code>order</code> value as part of its result. So, it seems we have to save it somewhere, which we choose to do with an outer <code>_order</code> variable that’s shared across scopes. When the second <code>then(..)</code> handler runs, <code>_order</code> will still be available for us to use.</p>
- <p>Don’t kid yourself: <strong>this is side-effect programming</strong>. <code>_order</code> is a side-effect of the first <code>then(..)</code>. We are crippled by the limitations of the Promise API mechanics and can’t share the state with the next step, so we just stick that state in an outer scope to share it.</p>
- <p>This is a bad idea. I don’t care how many times you’ve done this or how acceptable you think it is. <strong>It’s an anti-pattern code smell</strong>. Stop programming side-effects with your promises. It makes your code harder to reason about.</p>
- <p>I know how allergic many developers seem to be with respect to nesting, but the more appropriate way to handle this case is to nest the second <code>then(..)</code> in the scope where <code>order</code> still exists, as shown here:</p>
- <pre class="code">
- function getOrderDetails(orderID) {
- return db.find( "orders", orderID )
- .then( function(order){
- return db.find( "customers", order.customerID )
- // nested `then()` instead of flattened to outer chain,
- // so that we still have lexical scope access to `order`
- .then( function(customer){
- order.customer = customer;
- return order;
- } );
- } );
- }
- </pre>
- <p>Normally, nesting promise <code>then(..)</code>s is a bad idea, and even goes by the name “promise hell”.</p>
- <p>But in these cases where multiple values need to be available (in scope), a flattened vertical promise chain necessitates the worser evil of side-effects. Promise nesting is the better choice.</p>
- <h3 id="promise-chainitis">Promise Chain’itis</h3>
- <p>A promise models a value that may still be pending. The purpose is to factor time out of the picture so that you only have to think about the value and what you need to do with it — not the <em>time</em> you have to wait until it’s ready.</p>
- <p>The problem is that the mechanics of how promises work in JS means that the finalizing of a value is observable — you can tell when it happens. Though you shouldn’t be thinking about time, promises let you still think about time if you want to, and many people do it too much.</p>
- <p>The most obvious way we use our knowledge of when a promise resolves is to use this observation as a step in a flow control. For example:</p>
- <pre class="code">
- firstStep()
- .then( secondStep )
- .then( thirdStep );
- </pre>
- <p>Here, what we’re saying with the promise chain is that we want to delay the execution of the <code>secondStep()</code> function until after <code>firstStep()</code> has finished. You’ll notice that nothing here makes it obvious that we’re using a promise as a placeholder for a <em>future value</em>. Instead, we’re using promises kind of like event handlers to let us know when each step finishes.</p>
- <p>I used to be enormously excited about promise chains as flow control. But the longer I’ve programmed with promises, the more I’ve realized that long promise chains are actually a bad smell. I’ve almost come to the point where I’d say there should only ever be one <code>then(..)</code> in the chain. Using promise chains as a way to express flow control is a poor idea.</p>
- <p>Yes, yes, I know that <strong>literally almost everybody is doing this</strong>. Doesn’t matter, they’re all wrong.</p>
- <p>So, what should we do to express our multi-step flow control? Go back to nested callbacks? No.</p>
- <p>Use the <strong>synchronous-async pattern</strong>. If you’re not familiar, I’ve talked about this pattern in detail in several places, including:</p>
-
- <p>To boil this down, using either generators (with a runner), or <code>async function</code>s, we get the ability to express fundamentally asynchronous code in a synchronous fashion. This sync semantic works not only for success flow control but also error handling (ie, <code>try..catch</code>).</p>
- <p>Here’s how the previous promise-chain flow control code should be written:</p>
- <pre class="code">
- async function main() {
- await firstStep();
- await secondStep();
- await thirdStep();
- }
-
- // or:
-
- function *main() {
- yield firstStep();
- yield secondStep();
- yield thirdStep();
- }
- </pre>
- <p>We’re still relying on the fact that each step returns a promise, and we’re still waiting on each promise to resolve before moving onto the next step.</p>
- <p>Unfortunately, with these examples we’re missing not only articulation of the result values but also of any error handling. A bit more of a realistic example:</p>
- <pre class="code">
- async function main() {
- try {
- var val1 = await firstStep();
- var val2 = await secondStep( val1 );
- var val3 = await thirdStep( val1, val2 );
-
- console.log( "Final: ", val3 );
- }
- catch (err) {
- console.error( err );
- }
- }
- </pre>
- <p>This example clearly illustrates how we are using synchronous code semantics to express the async flow control (success and exception).</p>
- <p>Under the covers, something similar to the earlier chain, but not the same, is happening. <code>firstStep()</code> produces a promise, which is then <code>await</code>ed. That means, hidden from view, the engine puts a <code>then(..)</code> on it and waits for it to finish before resuming the paused <code>main()</code> function. It also unwraps the promise’s fulfillment value and gives it back so we can assign it to <code>val1</code>.</p>
- <p>When the <code>await</code> is called on the promise from the <code>secondStep(..)</code> call, it’s performing a <code>then(..)</code> against <em>that</em> promise, <strong>not</strong> the one that came from the <code>then(..)</code> called on the first. And so on.</p>
- <p>It may seem like pointless nuance, but that’s not the same thing as chaining one <code>then(..)</code> after another. It’s treating, explicitly, each step as a separate promise/<code>then(..)</code> pair.</p>
- <p>Conceptually that’s a much cleaner and easier to understand process than what happens when promises are chained multiple times. If you’ve ever tried to understand just how a promise chain is wired up, or explain it to someone else, you’ll know it’s not so simple. I won’t belabor those details here, but I’ve <a href="https://blog.getify.com/promises-part-5/#the-chains-that-bind-us">previously explained it in more detail</a> if you care to dig into it.</p>
- <p>The sync-async pattern above wipes away any of that unnecessary complexity. It also conveniently hides the <code>then(..)</code> calling altogether, because truthfully, that part is a wart. The <code>then(..)</code> on a promise is an unfortunate but necessary component of the Promise API design that was settled on for JS.</p>
- <p>I assert that, for the most part, we should try not to ever call <code>then(..)</code> explicitly. As much as possible, that should stay hidden as an implementation detail.</p>
- <p>Calling <code>then(..)</code> on a promise is a <strong>code smell and anti-pattern</strong>.</p>
- <p>I’m sure you’ve heard the admonition of “use the right tool for the job.” To put a slight twist on that, here the problem is more about using a tool inappropriately. It’s like if you held a hammer in your teeth and tried to drive in a nail. A hammer is better used if held in your hand.</p>
- <p>Instead of using the promise API for expressing async flow control, we should only use promises for what they’re good for: modeling future values in a time independent way. Sure, promises <em>can</em> be chained together. No, <strong>you shouldn’t be doing that</strong>. Let libraries and engines do the plumbing for you.</p>
- <p>There’s a better tool for the job of expressing flow control: the sync-async pattern of generators+promises or <code>async function</code>s.</p>
- <h3 id="summary">Summary</h3>
- <p>Summing up, using promises outside of the main notion of modeling a future value independent of time is probably wandering towards anti-patterns.</p>
- <p>Promises are powerful and transformative, but I urge you to take a step back and re-examine how you’re relying on them. They are best as a value/composition mechanism. Use them as such and stay away from all the wrong ways.</p>
|