|
| 1 | +project_path: /web/_project.yaml |
| 2 | +book_path: /web/fundamentals/_book.yaml |
| 3 | +description: Async functions allow you to write promise-based code as if it were synchronous |
| 4 | + |
| 5 | +{# wf_published_on: 2016-10-20 #} |
| 6 | +{# wf_updated_on: 2016-10-20 #} |
| 7 | + |
| 8 | +# Async functions - making promises friendly {: .page-title } |
| 9 | + |
| 10 | +{% include "web/_shared/contributors/jakearchibald.html" %} |
| 11 | + |
| 12 | +Async functions are enabled by default in Chrome 55 and they're quite frankly |
| 13 | +marvelous. They allow you to write promise-based code as if it were synchronous, |
| 14 | +but without blocking the main thread. They make your asynchronous code less |
| 15 | +"clever" and more readable. |
| 16 | + |
| 17 | +Async functions work like this: |
| 18 | + |
| 19 | + async function myFirstAsyncFunction() { |
| 20 | + try { |
| 21 | + const fulfilledValue = await promise; |
| 22 | + } |
| 23 | + catch (rejectedValue) { |
| 24 | + // … |
| 25 | + } |
| 26 | + } |
| 27 | + |
| 28 | +If you use the `async` keyword before a function definition, you can then use |
| 29 | +`await` within the function. When you `await` a promise, the function is paused |
| 30 | +until the promise settles. If the promise fulfills, you get the value back. If |
| 31 | +the promise rejects, the rejected value is thrown. |
| 32 | + |
| 33 | +Note: If you're unfamiliar with promises, check out [our |
| 34 | +promises guide](/web/fundamentals/getting-started/primers/promises). |
| 35 | + |
| 36 | +## Example: Logging a fetch |
| 37 | + |
| 38 | +Say we wanted to fetch a URL and log the response as text. Here's how it looks |
| 39 | +using promises: |
| 40 | + |
| 41 | + function logFetch(url) { |
| 42 | + return fetch(url) |
| 43 | + .then(response => response.text()) |
| 44 | + .then(text => { |
| 45 | + console.log(text); |
| 46 | + }).catch(err => { |
| 47 | + console.error('fetch failed', err); |
| 48 | + }); |
| 49 | + } |
| 50 | + |
| 51 | +And here's the same thing using async functions: |
| 52 | + |
| 53 | + async function logFetch(url) { |
| 54 | + try { |
| 55 | + const response = await fetch(url); |
| 56 | + console.log(await response.text()); |
| 57 | + } |
| 58 | + catch (err) { |
| 59 | + console.log('fetch failed', err); |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | +It's the same number of lines, but all the callbacks are gone. This makes it way |
| 64 | +easier to read, especially for those less familiar with promises. |
| 65 | + |
| 66 | +## Async return values |
| 67 | + |
| 68 | +Calling an async function returns a promise for whatever the function returns or |
| 69 | +throws. So with: |
| 70 | + |
| 71 | + // wait ms milliseconds |
| 72 | + function wait(ms) { |
| 73 | + return new Promise(r => setTimeout(r, ms)); |
| 74 | + } |
| 75 | + |
| 76 | + async function hello() { |
| 77 | + await wait(500); |
| 78 | + return 'world'; |
| 79 | + } |
| 80 | + |
| 81 | +…calling `hello()` returns a promise that *fulfills* with `"world"`. |
| 82 | + |
| 83 | + async function foo() { |
| 84 | + await wait(500); |
| 85 | + throw Error('bar'); |
| 86 | + } |
| 87 | + |
| 88 | +…calling `foo()` returns a promise that *rejects* with `Error('bar')`. |
| 89 | + |
| 90 | +## Example: Streaming a response |
| 91 | + |
| 92 | +The benefit of async functions increases in more complex examples. Say we wanted |
| 93 | +to stream a response while logging out the chunks, and return the final size. |
| 94 | + |
| 95 | +Note: The phrase "logging out the chunks" made me sick in my mouth. |
| 96 | + |
| 97 | +Here it is with promises: |
| 98 | + |
| 99 | + function getResponseSize(url) { |
| 100 | + return fetch(url).then(response => { |
| 101 | + const reader = response.body.getReader(); |
| 102 | + let total = 0; |
| 103 | + |
| 104 | + return reader.read().then(function processResult(result) { |
| 105 | + if (result.done) return total; |
| 106 | + |
| 107 | + const value = result.value; |
| 108 | + total += value.length; |
| 109 | + console.log('Received chunk', value); |
| 110 | + |
| 111 | + return reader.read().then(processResult); |
| 112 | + }) |
| 113 | + }); |
| 114 | + } |
| 115 | + |
| 116 | +Check me out, Jake "wielder of promises" Archibald. See how I'm calling |
| 117 | +`processResult` inside itself to set up an asynchronous loop? Writing that made |
| 118 | +me feel *very smart*. But like most "smart" code, you have to stare at it for |
| 119 | +ages to figure out what it's doing, like one of those magic-eye pictures from |
| 120 | +the 90's. |
| 121 | + |
| 122 | +Let's try that again with async functions: |
| 123 | + |
| 124 | + async function getResponseSize(url) { |
| 125 | + const response = await fetch(url); |
| 126 | + const reader = response.body.getReader(); |
| 127 | + let result = await reader.read(); |
| 128 | + let total = 0; |
| 129 | + |
| 130 | + while (!result.done) { |
| 131 | + const value = result.value; |
| 132 | + total += value.length; |
| 133 | + console.log('Received chunk', value); |
| 134 | + // get the next result |
| 135 | + result = await reader.read(); |
| 136 | + } |
| 137 | + |
| 138 | + return total; |
| 139 | + } |
| 140 | + |
| 141 | +All the "smart" is gone. The asynchronous loop that made me feel so smug is |
| 142 | +replaced with a trusty, boring, while-loop. Much better. In future, we'll get |
| 143 | +[async iterators](https://github.com/tc39/proposal-async-iteration){: .external}, |
| 144 | +which would |
| 145 | +[replace the `while` loop with a for-of loop](https://gist.github.com/jakearchibald/0b37865637daf884943cf88c2cba1376){: .external}, making it even neater. |
| 146 | + |
| 147 | +Note: I'm sort-of in love with streams. If you're unfamiliar with streaming, |
| 148 | +[check out my guide](https://jakearchibald.com/2016/streams-ftw/#streams-the-fetch-api){: .external}. |
| 149 | + |
| 150 | +## Other async function syntax |
| 151 | + |
| 152 | +We've seen `async function() {}` already, but the `async` keyword can be used |
| 153 | +with other function syntax: |
| 154 | + |
| 155 | +### Arrow functions |
| 156 | + |
| 157 | + // map some URLs to json-promises |
| 158 | + const jsonPromises = urls.map(async url => { |
| 159 | + const response = await fetch(url); |
| 160 | + return response.json(); |
| 161 | + }); |
| 162 | + |
| 163 | +Note: `array.map(func)` doesn't care that I gave it an async function, it just |
| 164 | +sees it as a function that returns a promise. It won't wait for the first |
| 165 | +function to complete before calling the second. |
| 166 | + |
| 167 | +### Object methods |
| 168 | + |
| 169 | + const storage = { |
| 170 | + async getAvatar(name) { |
| 171 | + const cache = await caches.open('avatars'); |
| 172 | + return cache.match(`/avatars/${name}.jpg`); |
| 173 | + } |
| 174 | + }; |
| 175 | + |
| 176 | + storage.getAvatar('jaffathecake').then(…); |
| 177 | + |
| 178 | +### Class methods |
| 179 | + |
| 180 | + class Storage { |
| 181 | + constructor() { |
| 182 | + this.cachePromise = caches.open('avatars'); |
| 183 | + } |
| 184 | + |
| 185 | + async getAvatar(name) { |
| 186 | + const cache = await this.cachePromise; |
| 187 | + return cache.match(`/avatars/${name}.jpg`); |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + const storage = new Storage(); |
| 192 | + storage.getAvatar('jaffathecake').then(…); |
| 193 | + |
| 194 | +Note: Class constructors and getters/settings cannot be async. |
| 195 | + |
| 196 | +## Careful! Avoid going too sequential |
| 197 | + |
| 198 | +Although you're writing code that looks synchronous, ensure you don't miss the |
| 199 | +opportunity to do things in parallel. |
| 200 | + |
| 201 | + async function series() { |
| 202 | + await wait(500); |
| 203 | + await wait(500); |
| 204 | + return "done!"; |
| 205 | + } |
| 206 | + |
| 207 | +The above takes 1000ms to complete, whereas: |
| 208 | + |
| 209 | + async function parallel() { |
| 210 | + const wait1 = wait(500); |
| 211 | + const wait2 = wait(500); |
| 212 | + await wait1; |
| 213 | + await wait2; |
| 214 | + return "done!"; |
| 215 | + } |
| 216 | + |
| 217 | +…the above takes 500ms to complete, because both waits happen at the same time. |
| 218 | +Let's look at a practical example… |
| 219 | + |
| 220 | +### Example: Outputting fetches in order |
| 221 | + |
| 222 | +Say we wanted to fetch a series URLs and log them as soon as possible, in the |
| 223 | +correct order. |
| 224 | + |
| 225 | +*Deep breath* - here's how that looks with promises: |
| 226 | + |
| 227 | + function logInOrder(urls) { |
| 228 | + // fetch all the URLs |
| 229 | + const textPromises = urls.map(url => { |
| 230 | + return fetch(url).then(response => response.text()); |
| 231 | + }); |
| 232 | + |
| 233 | + // log them in order |
| 234 | + textPromises.reduce((chain, textPromise) => { |
| 235 | + return chain.then(() => textPromise) |
| 236 | + .then(text => console.log(text)); |
| 237 | + }, Promise.resolve()); |
| 238 | + } |
| 239 | + |
| 240 | +Yeah, that's right, I'm using `reduce` to chain a sequence of promises. I'm *so |
| 241 | +smart*. But this is a bit of *so smart* coding we're better off without. |
| 242 | + |
| 243 | +However, when converting the above to an async function, it's tempting to go |
| 244 | +*too sequential*: |
| 245 | + |
| 246 | +<span class="compare-worse">Not recommended</span> - too sequential |
| 247 | + |
| 248 | + async function logInOrder(urls) { |
| 249 | + for (const url of urls) { |
| 250 | + const response = await fetch(url); |
| 251 | + console.log(await response.text()); |
| 252 | + } |
| 253 | + } |
| 254 | + |
| 255 | +Looks much neater, but my second fetch doesn't begin until my first fetch has |
| 256 | +been fully read, and so on. This is much slower than the promises example that |
| 257 | +performs the fetches in parallel. Thankfully there's an ideal middle-ground: |
| 258 | + |
| 259 | +<span class="compare-better">Recommended</span> - nice and parallel |
| 260 | + |
| 261 | + async function logInOrder(urls) { |
| 262 | + // fetch all the URLs in parallel |
| 263 | + const textPromises = urls.map(async url => { |
| 264 | + const response = await fetch(url); |
| 265 | + return response.text(); |
| 266 | + }); |
| 267 | + |
| 268 | + // log them in sequence |
| 269 | + for (const textPromise of textPromises) { |
| 270 | + console.log(await textPromise); |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | +In this example, the URLs are fetched and read in parallel, but the "smart" |
| 275 | +`reduce` bit is replaced with a standard, boring, readable for-loop. |
| 276 | + |
| 277 | +## Browser support & workarounds |
| 278 | + |
| 279 | +At time of writing, async functions are enabled by default in Chrome 55, but |
| 280 | +they're being developed in all the main browsers: |
| 281 | + |
| 282 | +* Edge - [In build 14342+ behind a flag](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/asyncfunctions/) |
| 283 | +* Firefox - [active development](https://bugzilla.mozilla.org/show_bug.cgi?id=1185106) |
| 284 | +* Safari - [active development](https://bugs.webkit.org/show_bug.cgi?id=156147) |
| 285 | + |
| 286 | +### Workaround - Generators |
| 287 | + |
| 288 | +If you're targeting browsers that support generators (which includes |
| 289 | +[the latest version of every major browser](http://kangax.github.io/compat-table/es6/#test-generators){:.external} |
| 290 | +) you can sort-of polyfill async functions. |
| 291 | + |
| 292 | +[Babel](https://babeljs.io/){: .external} will do this for you, |
| 293 | +[here's an example via the Babel REPL](https://goo.gl/0Cg1Sq){: .external} |
| 294 | +- note how similar the transpiled code is. This transformation is part of |
| 295 | +[Babel's stage 3 preset](http://babeljs.io/docs/plugins/preset-stage-3/){: .external}. |
| 296 | + |
| 297 | +Note: Babel REPL is fun to say. Try it. |
| 298 | + |
| 299 | +I recommend the transpiling approach, because you can just turn it off once your |
| 300 | +target browsers support async functions, but if you *really* don't want to use a |
| 301 | +transpiler, you can take |
| 302 | +[Babel's polyfill](https://gist.github.com/jakearchibald/edbc78f73f7df4f7f3182b3c7e522d25){: .external} |
| 303 | +and use it yourself. Instead of: |
| 304 | + |
| 305 | + async function slowEcho(val) { |
| 306 | + await wait(1000); |
| 307 | + return val; |
| 308 | + } |
| 309 | + |
| 310 | +…you'd include [the polyfill](https://gist.github.com/jakearchibald/edbc78f73f7df4f7f3182b3c7e522d25){: .external} |
| 311 | +and write: |
| 312 | + |
| 313 | + const slowEcho = createAsyncFunction(function*(val) { |
| 314 | + yield wait(1000); |
| 315 | + return val; |
| 316 | + }); |
| 317 | + |
| 318 | +Note that you have to pass a generator (`function*`) to `createAsyncFunction`, |
| 319 | +and use `yield` instead of `await`. Other than that it works the same. |
| 320 | + |
| 321 | +### Workaround - regenerator |
| 322 | + |
| 323 | +If you're targeting older browsers, Babel can also transpile generators, |
| 324 | +allowing you to use async functions all the way down to IE8. To do this you need |
| 325 | +[Babel's stage 3 preset](http://babeljs.io/docs/plugins/preset-stage-3/){: .external} |
| 326 | +*and* the [ES2015 preset](http://babeljs.io/docs/plugins/preset-es2015/){: .external}. |
| 327 | + |
| 328 | +The [output is not as pretty](https://goo.gl/jlXboV), so watch out for |
| 329 | +code-bloat. |
| 330 | + |
| 331 | +## Async all the things! |
| 332 | + |
| 333 | +Once async functions land across all browsers, use them on every |
| 334 | +promise-returning function! Not only do they make your code tider, but it makes |
| 335 | +sure that function will *always* return a promise. |
| 336 | + |
| 337 | +I got really excited about async functions [back in |
| 338 | +2014](https://jakearchibald.com/2014/es7-async-functions/){: .external}, and |
| 339 | +it's great to see them land, for real, in browsers. Whoop! |
0 commit comments