DEV Community

Cat
Cat

Posted on

Explain Async/Await Like I'm Five

Please. Been trying to find a clear explanation as to why Async/Await > Promises or so lots of devs say.

Top comments (24)

Collapse
 
fluffynuts profile image
Davyd McColl • Edited

JavaScript, by design, is not multithreaded. So unlike your operating system, it can't do two things at once.
However, there are times we need to wait for stuff to come back from the server. In this case, JavaScript can ask the browser to do the work and be notified when that work is done

This is asynchronous programming - having something done in the background and getting notified when it's done.

Originally, we did this with callbacks - so we'd say "hey browser, please do this work, and when you're done, here's a function you can call with the results"

The problem with callbacks is that we often have to do a few things in the background (asynchronously), so the code starts to get difficult to understand, with many layers of nested callbacks.

Along came the promise, which meant that our code could flatten out a bit. Callback code like this:

$.get("/api/request1", function(result1) {
  $.get("/api/request2?q=" + result1, function(result2) {
    console.log(result1 + " - " + result2);
  })
);
Enter fullscreen mode Exit fullscreen mode

Could be a little simpler:

$.get("/api/request1").then(function(result1) {
  return $.get("/api/request2?q=" + result1);
}).then(function(result2) {
  console.log(result1 + " - " + result2);
});
Enter fullscreen mode Exit fullscreen mode

(EDIT: as noted below, the code above has a bug; I'm not going to remove it, because that would invalidate comments below, but do check out fixes for this code, as shown below!)

Which was a little easier to read and write (and error handling was neater, with .catch())

async/await is a nice way that modern JavaScript let's us use promises with even less code and nesting:

var result1 = await $.get("/api/request1");
var result2 = await $.get("/api/request2?q=" + result1);
console.log(result1 + " - " + result2);
Enter fullscreen mode Exit fullscreen mode

And you can use try/catch to handle errors.

Under the hood, helpers like Babel and TypeScript translate the code above into a version a lot like the promises version, using an old, clever JavaScript feature called generator functions. Don't worry about it too much now though (:

Hope this helps. Apologies for any typos - I'm doing this from my phone.

Collapse
 
jaminfarr profile image
Ben Farr • Edited

Edit: Sorry I didn't see the example above the flattened one which is the same as one of my versions.

Your promise example has a bug but it's a really good example where async/await shines over promises.

$.get("/api/request1").then(function(result1) {
  return $.get("/api/request2?q=" + result1);
}).then(function(result2) {
  // result1 isn't in scope here and will crash
  console.log(result1 + " - " + result2);
});
Enter fullscreen mode Exit fullscreen mode

result1 isn't accessible in the final then. With promises you lose the simplicity of the .then chain if you want both the first return value and to use it as a trigger for a follow-up request.

These are some ways to do it with promises.

// Store result1 in a higher scope
let result1;

$.get("/api/request1")
  .then(function (_result1) {
    result1 = _result1;
    return $.get("/api/request2?q=" + result1);
  })
  .then(function (result2) {
    console.log(result1 + " - " + result2);
  });

// Bring the scope of the result2 then within the result1 then
$.get("/api/request1").then(function (result1) {
  return $.get("/api/request2?q=" + result1).then(function (result2) {
    console.log(result1 + " - " + result2);
  });
});

// Add result1 to second request's response value
$.get("/api/request1")
  .then(function (result1) {
    return $.get("/api/request2?q=" + result1).then(function (result2) {
      return [result1, result2];
    });
  })
  .then(function (results) {
    console.log(results[0] + " - " + results[1]);
  });

// Use request1 as trigger for request2 and combine the requests/results
const request1 = $.get("/api/request1");
const request2 = request1.then(function (result1) {
  return $.get("/api/request2?q=" + result1);
});

Promise.all([request1, request2]).then(function (results) {
  console.log(results[0] + " - " + results[1]);
});
Enter fullscreen mode Exit fullscreen mode

With async/await, as Davyd has shown above, you have a single scope. No workarounds are needed.

var result1 = await $.get("/api/request1");
var result2 = await $.get("/api/request2?q=" + result1);
console.log(result1 + " - " + result2);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
fluffynuts profile image
Davyd McColl • Edited

Nice catch. I could hardly see all the text on my phone when I was typing. Was bound to have at least one glaring flaw (:

Now, I'm at a pc, I think I'd probably correct to:

$.get("/api/request1").then(function(result1) {
  return $.get("/api/request2?q=" + result1).then(function(result2) {
    return { result1, result2 };
  });
}).then(function(results) {
  console.log(results.result1 + " - " + results.result22);
});

(assuming I can use short notation for objects in this particular js-world)

And I think that illustrates even better why async/await is a win (:

Collapse
 
johnmunsch profile image
John Munsch

I have a reputation to uphold here: stackoverflow.com/a/1638961/31899 So let me give it a shot. Ahem! :)

I am your faithful dog JS. I love you with all my heart, but normally when you go off to school. I go do other stuff. I find React the cat and bark at him. That's fun because he's getting really old. I nap. I eat. I drive the dreaded "mail man" away from our house. Then, when you get home again, we play! Hooray!

But today, you said, "Await me and I will give you this bone." So I have not moved. I am a good dog. I am a very good dog. I will stay here and not do anything else until you get back and then I will get the bone and we will play!

That, is Async/Await for a five year old.

Collapse
 
moose_said profile image
Mostafa Said

Love it! :D

Collapse
 
cat profile image
Cat

LMAO I love this!!!

Collapse
 
avalander profile image
Avalander

After many discussions about it, I've come to the conclusion that people prefer async/await over promises because they don't want to do function composition and want to write synchronous imperative code.

Collapse
 
qm3ster profile image
Mihail Malo

There's a time and a place for everything.

Collapse
 
hunter profile image
Hunter Henrichsen • Edited

I'll throw my hat into the ring.

Let's say you're running a race, and there are water stations set up along the way. You can keep running down the main path, or hop off to the side to one of the water stations. Some of them have lines, and you have to wait your turn before you can get water and go back to the main path. To await means to be one of the people waiting in line, because you're *wait*ing for the person in front of you to finish what they're doing. For a minute, the two runners are joined together and can only go as fast as the first of them. Waiting in line makes it so that other people can queue up behind us (we have to be marked async, which means other people can await us before we can await someone else).

In terms of code, this makes it much cleaner and straightforward, as I think Davyd has demonstrated.

Collapse
 
louy2 profile image
Yufan Lou

Because async/await is actually one level higher than Promise. An async function is not a Promise but a () => Promise. In other words, an async function is a lazy Promise.

// Promise
let p0 = new Promise((res, rej) => {
  res(longRunningBlockingTask());
});
p0.then(x => x + 1)
  .then(console.log)
  .catch(console.log)
  .finally(console.log('cleanup'));

Note that we kind of hit a dead-end above. Even though we've created p0 to store the Promise, we cannot rerun p0. The most we can do is attach more thens. We cannot cancel it either.

// async/await
let p = async () => ;
let f = async () => {
  try { console.log(await p()) } catch (err) { console.log(err) }
}
f().catch(console.log)
f().catch(console.log)

We still cannot cancel it, but at least we can rerun it.

A more detailed explanation can be found in the Wiki of Fluture.

Collapse
 
sebbdk profile image
Sebastian Vargr • Edited

Async/Await is syntactic sugar around promises.

So think of them like the same thing.

The two can also be used intertwined, E.g. one might use Promise.all to wait for two async functions to return in parallel.

Collapse
 
qm3ster profile image
Mihail Malo

A Promise is a "handle" to a value that may or may not already be available.
It's better than a callback, because you can start the asynchronous process first, and think about what you will do with the outcome of it later.

do_thing(result => console.log(result))
// <- Here is already too late to change your mind. And you don't have access to `result` either.
const promise = do_thing()
if (logging) promise.then(result => console.log(result))
promise.then(result => {/* still available here, without running `do_thing` twice */})

but what is await? It allows you to wait until the value of a promise is available within synchronous-looking code, with all the usual imperative constructs.

const promise = do_thing()
if (logging) console.log(await promise)
something_else(await promise)

How is this achieved? When you execute "await", your function pauses, and allows other things to run.
Then, when the awaited result is ready, the function continues execution from that point onward, as if "await" was just a function that took a long time.
Think of it like the code before the await and the code after the await are two separate functions, just keeping the same variable scope.
That's why it can only be used in async functions: since the function doesn't "finish" (return the actual value and end forever) when called, but later, it also must return a Promise. The async annotation is just so that people are not confused why the function returns a Promise when called instead of what was actually passed to return statements in it.

Understanding that it's Promises underneath, and that each await stops the function and puts it in a queue tells us how we should optimize for good performance:

  1. For example, we shouldn't await the same value multiple times where we can easily avoid it:
const value = await do_thing() // fix for our example above
if (logging) console.log(value)
something_else(value)
  1. But we shouldn't immediately await every Promise we get our hands on, because there might be more work we can spawn before we need the result!
const urls = ['/a', '/b', '/c']
async bad_fetch(){
  const results = []
  for (const url of urls) {
    const res = await fetch(url)
    const data = await res.json()
    results.push(data.property)
  }
  return results
}
async good_fetch() {
  const results = []
  // now we start all requests, as well as parsing of their data, before "yeeting" from the function
  for (const data of await Promise.all(urls.map(url => fetch(url).then(x=>x.json()))) {
    results.push(data.property)
  }
  return results
}

The same, but without then and push (previous was written to be similar to the bad example)

const good_fetch = async () =>   Promise.all(
    urls.map( async url =>
      (await (await fetch(url)).json()).data
  )
Collapse
 
qm3ster profile image
Mihail Malo

Gotchas

All throws in a function marked async are rejections, never throws from the initial function call.

const throws_fast = async () => {console.log('running');throw 'fast'}
let p
try {p = throws_fast()} catch (err) {console.log('synchronous catch')}
console.log('first catch over')
try {await p} catch (err) {console.log('awaited catch')}

This outputs

running <- so the body DID actually run synchronously on call
first catch over <- then things after the call
awaited catch <- and only in await we get the error

But this isn't always true for all Promise-returning functions!

const throws_fast = async () => {console.log('running');throw 'fast'}
const throws_fast_really = (fast) => {if (fast) throw 'really fast'; return throws_fast()}
let p
try {p = throws_fast_really()} catch (err) {console.log('synchronous catch')}
console.log('first catch over')
try {await p} catch (err) {console.log('awaited catch')}
console.log('second attempt!')
let p2
try {p2 = throws_fast_really(true)} catch (err) {console.log('synchronous catch')}
console.log('first catch over')
try {await p2} catch (err) {console.log('awaited catch')}
running
first catch over
awaited catch
second attempt!
synchronous catch
first catch over
Collapse
 
richardsefton profile image
RichardSefton

Promises and async functions are like you're code is a hard worker. But it has too much to do by itself so it says to it's useless coworker sitting doing nothing "do this and come back to me with the results while I carry on with this"

Await is when your code needs the result from that so you say await that result.

Side note. If it's JavaScript you can only call await inside an async function.

Collapse
 
patricktingen profile image
Patrick Tingen

So if you have this code (from Davyd above)

var result1 = await $.get("/api/request1");
var result2 = await $.get("/api/request2?q=" + result1);
console.log(result1 + " - " + result2);

Does the code wait until result1 is set and then continue with result2 or does it just make both calls and wait until both are ready?

Collapse
 
danieliverant profile image
Daniel Liverant

the code waits until result1 is set and just then continue to result2.
if you want to wait for them both to complete and then continue your code you need to use
await Promise.all([result1, result2])

Collapse
 
minhng97 profile image
Minh Nguyen • Edited

Async:

async function loadThis(){
 var loadedData = await fetch(URL); //wait for this and not execute the next line
return loadedData;
}
alert(loadThis);

Promise:

var s = function(){return Promise.resolve(URL).then(result => alert(result);
}
s()
Collapse
 
cutiko profile image
Erick Navarro

Imagine you drop a pillow, that pillow falls, right?
Then imagine if you make that pillow async and in the middle of it you say await, the pillow stops in the middle of the air and you can do something else like change the pillow cover, only when you are done, the pillow continues falling.

Collapse
 
manontherun profile image
Phil Gemellas

Try this video:
youtube.com/watch?v=xTjx3q2Nm1w
By far the most detailed explanation on asynchronous JavaScript.
Also don't forget to bookmark the channel. Codesmith.io is one of the most valuable resources IMHO.

Collapse
 
raulismasiukas profile image
Raulis Masiukas

Code can be synchronous, where a line of code has to be completed before you can move on to the next one, and asynchronous, where the program will move on to the next line without waiting for the previous line to finish.
Async/await makes asynchronous code feel synchronous, aka won't move to the next line until the previous line finishes execution, whilst in fact it's asynchronous in the background. It's much easier to read code line by line than having to parse the cognitive complexity that promises introduce.

Collapse
 
danieliverant profile image
Daniel Liverant

Actually this is a very good question that shows the problem with the way programmers learn to code (no judgment here).
If you understand what was before promises, and understand the fundamentals of JS (event loop etc...)
you will have no problem understanding await/async, but programmers always skip to the last piece of technology instead of learning the basics first.

the original question was already answered so I just wanted to add this.

Collapse
 
cat profile image
Cat

I think the newbies (such as myself) don't really know any better and everything they look up features the latest and greatest shiny new thing.

Collapse
 
savagepixie profile image
SavagePixie

Async/await isn't better than promises. It's more imperative. So naturally people who prefer imperative code will prefer async/await over promises.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.