There’s a special syntax to work with promises in a more comfortable fashion, called “async/await”. It’s surprisingly easy to understand and use. Let’s start with the async
keyword. It can be placed before a function, like this:
async function f() { return 1; }
The word “async” before a function means one simple thing: a function always returns a promise. Even If a function actually returns a non-promise value, prepending the function definition with the “async” keyword directs Javascript to automatically wrap that value in a resolved promise. For instance, the code above returns a resolved promise with the result of 1
, let’s test it:
async function f() { return 1; } f().then(alert); // 1
…We could explicitly return a promise, that would be the same:
async function f() { return Promise.resolve(1); } f().then(alert); // 1
So, async
ensures that the function returns a promise, and wraps non-promises in it. Simple enough, right? But not only that. There’s another keyword, await
, that works only inside async
functions, and it’s pretty cool.
// works only inside async functions let value = await promise;
The keyword await
makes JavaScript wait until that promise settles and returns its result. Here’s an example with a promise that resolves in 1
second:
async function f() { let promise = new Promise((resolve, reject) => { setTimeout(() => resolve("done!"), 1000) }); let result = await promise; // wait till the promise resolves (*) alert(result); // "done!" } f();
The function execution “pauses” at the line (*)
and resumes when the promise settles, with result becoming its result. So the code above shows “done!” in one second.
Let’s emphasize: await
literally makes JavaScript wait until the promise settles, and then go on with the result. That doesn’t cost any CPU resources, because the engine can do other jobs meanwhile: execute other scripts, handle events etc.
It’s just a more elegant syntax of getting the promise result than promise.then
, easier to read and write.
If we try to use await
in non-async function, that would be a syntax error:
function f() { let promise = Promise.resolve(1); let result = await promise; // Syntax error }
We will get this error if we do not put async
before a function. As said, await
only works inside an async
function.
Like promise.then
, await
allows to use thenable objects (those with a callable then method). Again, the idea is that a 3rd-party object may not be a promise, but promise-compatible: if it supports .then
, that’s enough to use with await
.
For instance, here await
accepts new Thenable(1)
:
class Thenable { constructor(num) { this.num = num; } then(resolve, reject) { alert(resolve); // function() { native code } // resolve with this.num*2 after 1000ms setTimeout(() => resolve(this.num * 2), 1000); // (*) } }; async function f() { // waits for 1 second, then result becomes 2 let result = await new Thenable(1); alert(result); } f();
If await
gets a non-promise object with .then
, it calls that method providing native functions resolve, reject as arguments. Then await
waits until one of them is called (in the example above it happens in the line (*)
) and then proceeds with the result.
A class method can also be async, just put async before it.
class Waiter { async wait() { return await Promise.resolve(1); } } new Waiter() .wait() .then(alert); // 1
The meaning is the same: it ensures that the returned value is a promise and enables await
.
If a promise resolves normally, then await promise returns the result. But in case of a rejection, it throws the error, just as if there were a throw statement at that line.
The code:
async function f() { await Promise.reject(new Error("Whoops!")); }
…Is the same as this:
async function f() { throw new Error("Whoops!"); }
In real situations, the promise may take some time before it rejects. So await
will wait, and then throw an error. We can catch that error using try..catch
, the same way as a regular throw:
async function f() { try { let response = await fetch('http://no-such-url'); } catch(err) { alert(err); // TypeError: failed to fetch } } f();
In case of an error, the control jumps to the catch
block. We can also wrap multiple lines:
async function f() { try { let response = await fetch('/no-user-here'); let user = await response.json(); } catch(err) { // catches errors both in fetch and response.json alert(err); } } f();
If we don’t have try..catch
, then the promise generated by the call of the async function f()
becomes rejected. We can append .catch
to handle it:
async function f() { let response = await fetch('http://no-such-url'); } // f() becomes a rejected promise f().catch(alert); // TypeError: failed to fetch // (*)
When we use async
/await
, we rarely need .then
, because await
handles the waiting for us. And we can use a regular try..catch
instead of .catch
. That’s usually (not always) more convenient.
But at the top level of the code, when we’re outside of any async
function, we’re syntactically unable to use await
, so it’s a normal practice to add .then
/catch
to handle the final result or falling-through errors.
When we need to wait for multiple promises, we can wrap them in Promise.all
and then await
:
// wait for the array of results let results = await Promise.all([ fetch(url1), fetch(url2), ... ]);