Skip to content
This repository was archived by the owner on Aug 10, 2022. It is now read-only.

Article: async functions #3617

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/content/en/fundamentals/getting-started/_toc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ toc:
path: /web/fundamentals/getting-started/primers/service-workers
- title: "JavaScript Promises: an Introduction"
path: /web/fundamentals/getting-started/primers/promises
- title: "JavaScript Async Functions"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You only need to quote if you're using a 'fun' character like :

path: /web/fundamentals/getting-started/primers/async-functions
- title: "Shadow DOM v1: Self-Contained Web Components"
path: /web/fundamentals/getting-started/primers/shadowdom
- title: "Custom Elements v1: Reusable Web Components"
Expand Down
345 changes: 345 additions & 0 deletions src/content/en/fundamentals/getting-started/primers/async-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
project_path: /web/_project.yaml
book_path: /web/fundamentals/_book.yaml
description: Async functions allow you to write promise-based code as if it were synchronous

{# wf_published_on: 2016-10-20 #}
{# wf_updated_on: 2016-10-20 #}

# Async functions - making promises friendly {: .page-title }

{% include "web/_shared/contributors/jakearchibald.html" %}

Async functions are enabled by default in Chrome 55 and they're quite frankly
marvelous. They allow you to write promise-based code as if it were synchronous,
but without blocking the main thread. They make your asynchronous code less
"clever" and more readable.

Async functions work like this:

async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
}
catch (rejectedValue) {
// …
}
Copy link
Contributor

@ebidel ebidel Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so because async functions always return a promise, you don't explicitly need a return val? Might want to mention that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Welllllll you need to return what you want the promise to resolve to, else it resolves to void the same way functions return void if you don't have an explicit return.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see a lot of code samples like this, with the try/catch. I worry that presenting async functions like this may make people think they always need a try/catch block inside their async function body, which definitely isn't true. Maybe the initial example should be a chain of awaiting multiple things, or even a loop, and then explain the exceptions/rejection duality later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try and pick up on this elsewhere in the article. I kinda like opening with how fulfill/reject values map in async functions.

}

If you use the `async` keyword before a function definition, you can then use
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not for this article, but why is async even necessary? (Why is await appearing in the body not sufficient?) Is this a convenience to the JS engine?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the reasons are similar to why function()* is necessary for generators, but I'm not 100% sure.

FWIW I've been caught out by generators in Python in the past. It wasn't clear to me that a function would behave very differently because it has a yield hidden away in it somewhere.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a few reasons:

  • When you have such drastically different modes, it's a benefit to readability to have it called out when the function starts (as Jake says above).
  • You can have an async function which doesn't contain any await; the semantics will be kinda sorta like if return foo was return Promise.resolve(foo)
  • await is a new keyword, but we can't "just" add keywords. They might already be used as variables. Scoping the keyword-ness to async functions is one technique that await uses. (async uses other techniques, e.g., that if it's followed by function, it would've been a syntax error.)

`await` within the function. When you `await` a promise, the function is paused
until the promise settles. If the promise fulfills, you get the value back. If
the promise rejects, the rejected value is thrown.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right paragraph to lead with? Maybe the article should open with an explanation and examples of, "you can await a bunch of stuff in a row!" and then get into the nitty gritty of exception handling later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really feel nitty gritty to me, it's kinda "the basics". I'm going to move the stuff about return values to after the first example, so readers will get to the first example quicker.


Note: If you're unfamiliar with promises, check out [our
promises guide](/web/fundamentals/getting-started/primers/promises).

## Example: Logging a fetch

Say we wanted to fetch a URL and log the response as text. Here's how it looks
using promises:

function logFetch(url) {
return fetch(url)
.then(response => response.text())
.then(text => {
console.log(text);
}).catch(err => {
console.error('fetch failed', err);
});
}

And here's the same thing using async functions:

async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
}
catch (err) {
console.log('fetch failed', err);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome example, much cleaner and gives a good use case for exception handling/rejection. Maybe you should move that opening try/catch example to just above this.


It's the same number of lines, but all the callbacks are gone. This makes it way
easier to read, especially for those less familiar with promises.

## Async return values

Calling an async function returns a promise for whatever the function returns or
throws. So with:

// wait ms milliseconds
function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}

async function hello() {
await wait(500);
return 'world';
}

…calling `hello()` returns a promise that *fulfills* with `"world"`.

async function foo() {
await wait(500);
throw Error('bar');
}

…calling `foo()` returns a promise that *rejects* with `Error('bar')`.

## Example: Streaming a response

The benefit of async functions increases in more complex examples. Say we wanted
to stream a response while logging out the chunks, and return the final size.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You just had to mention streams :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep on streamin'

Note: The phrase "logging out the chunks" made me sick in my mouth.

Here it is with promises:

function getResponseSize(url) {
return fetch(url).then(response => {
const reader = response.body.getReader();
let total = 0;

return reader.read().then(function processResult(result) {
if (result.done) return total;

const value = result.value;
total += value.length;
console.log('Received chunk', value);

return reader.read().then(processResult);
})
});
}

Check me out, Jake "wielder of promises" Archibald. See how I'm calling
`processResult` inside itself to set up an asynchronous loop? Writing that made
me feel *very smart*. But like most "smart" code, you have to stare at it for
ages to figure out what it's doing, like one of those magic-eye pictures from
the 90's.

Let's try that again with async functions:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we also add the catch to show how to handle errors?


async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;

while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}

return total;
}

All the "smart" is gone. The asynchronous loop that made me feel so smug is
replaced with a trusty, boring, while-loop. Much better. In future, we'll get
[async iterators](https://github.com/tc39/proposal-async-iteration){:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't break {: something } attributes, the parser isn't smart enough to handle this. :( keep them all on one line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damnit, that's what my auto-wrapping plugin does. Will watch out for this, cheers!

.external}, which would [replace the `while`
loop with a for-of loop](https://gist.github.com/jakearchibald/0b37865637daf884943cf88c2cba1376){:
.external}, making it even neater.

Note: I'm sort-of in love with streams. If you're unfamiliar with streaming,
[check out my
guide](https://jakearchibald.com/2016/streams-ftw/#streams-the-fetch-api){:
.external}.

## Other async function syntax

We've seen `async function() {}` already, but the `async` keyword can be used
with other function syntax:

### Arrow functions

// map some URLs to json-promises
const jsonPromises = urls.map(async url => {
const response = await fetch(url);
return response.json();
});

Note: `array.map(func)` doesn't care that I gave it an async function, it just
sees it as a function that returns a promise. It won't wait for the first
function to complete before calling the second.

### Object methods

const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};

storage.getAvatar('jaffathecake').then(…);

### Class methods

class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}

async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}

const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);

Note: Class constructors and getters/settings cannot be async.

## Careful! Avoid going too sequential

Although you're writing code that looks synchronous, ensure you don't miss the
opportunity to do things in parallel.

async function series() {
await wait(500);
await wait(500);
return "done!";
}

The above takes 1000ms to complete, whereas:

async function parallel() {
const wait1 = wait(500);
const wait2 = wait(500);
await wait1;
await wait2;
return "done!";
}

…the above takes 500ms to complete, because both waits happen at the same time.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to call out observables are different than promises as they always start no matter what when you call them unlike observables.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why observables would be mentioned here.

Let's look at a practical example…

### Example: Outputting fetches in order

Say we wanted to fetch a series URLs and log them as soon as possible, in the
correct order.

*Deep breath* - here's how that looks with promises:

function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});

// log them in order
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}

Yeah, that's right, I'm using `reduce` to chain a sequence of promises. I'm *so
smart*. But this is a bit of *so smart* coding we're better off without.

However, when converting the above to an async function, it's tempting to go
*too sequential*:

<span class="compare-worse">Not recommended</span> - too sequential

async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}

Looks much neater, but my second fetch doesn't begin until my first fetch has
been fully read, and so on. This is much slower than the promises example that
performs the fetches in parallel. Thankfully there's an ideal middle-ground:

<span class="compare-better">Recommended</span> - nice and parallel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 nice use


async function logInOrder(urls) {
// fetch all the URLs in parallel
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});

// log them in sequence
for (const textPromise of textPromises) {
console.log(await textPromise);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great example

}

In this example, the URLs are fetched and read in parallel, but the "smart"
`reduce` bit is replaced with a standard, boring, readable for-loop.

## Browser support & workarounds

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using a framebox here to embed the chromestatus info:
https://www.chromestatus.com/feature/5643236399906816?embed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this a bit huge for an embed?

At time of writing, async functions are enabled by default in Chrome 55, but
they're being developed in all the main browsers:

* Edge - [In build 14342+ behind a flag](https://developer.microsoft.com/en-us/microsoft-edge/platform/status/asyncfunctions/)
* Firefox - [active development](https://bugzilla.mozilla.org/show_bug.cgi?id=1185106)
* Safari - [active development](https://bugs.webkit.org/show_bug.cgi?id=156147)

### Workaround - Generators

If you're targeting browsers that support generators (which includes [the latest
version of every major
browser](http://kangax.github.io/compat-table/es6/#test-generators){:
.external}) you can sort-of polyfill async functions.

[Babel](https://babeljs.io/){: .external} will do this for you, [here's an
example via the Babel REPL](https://goo.gl/0Cg1Sq){: .external} - note how
similar the transpiled code is. This transformation is part of [Babel's stage 3
preset](http://babeljs.io/docs/plugins/preset-stage-3/){: .external}.

Note: Babel REPL is fun to say. Try it.

I recommend the transpiling approach, because you can just turn it off once your
target browsers support async functions, but if you *really* don't want to use a
transpiler, you can take [Babel's
polyfill](https://gist.github.com/jakearchibald/edbc78f73f7df4f7f3182b3c7e522d25){:
.external} and use it yourself. Instead of:

async function slowEcho(val) {
await wait(1000);
return val;
}

…you'd include [the
polyfill](https://gist.github.com/jakearchibald/edbc78f73f7df4f7f3182b3c7e522d25){:
.external} and write:

const slowEcho = createAsyncFunction(function*(val) {
yield wait(1000);
return val;
});

Note that you have to pass a generator (`function*`) to `createAsyncFunction`,
and use `yield` instead of `await`. Other than that it works the same.

### Workaround - regenerator

If you're targeting older browsers, Babel can also transpile generators,
allowing you to use async functions all the way down to IE8. To do this you need
[Babel's stage 3 preset](http://babeljs.io/docs/plugins/preset-stage-3/){:
.external} *and* the [ES2015
preset](http://babeljs.io/docs/plugins/preset-es2015/){: .external}.

The [output is not as pretty](https://goo.gl/jlXboV), so watch out for
code-bloat.

## Async all the things!

Once async functions land across all browsers, use them on every
promise-returning function! Not only do they make your code tider, but it makes
sure that function will *always* return a promise.

Copy link
Contributor

@addyosmani addyosmani Oct 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for calling out the path to older browser support here too. Link to Babel docs for transpiling generators here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah good point

I got really excited about async functions [back in
2014](https://jakearchibald.com/2014/es7-async-functions/){: .external}, and
it's great to see them land, for real, in browsers. Whoop!