0

I'm starting to learn asynchronous Javascript and I'm really confused.

To be honest, the async/await approach seems very logical to me. We need to let the runtime know we're doing an asynchronous operation so it can handle it accordingly. But why don't we need to do the same when using the .then() method? I mean, if Javascript was already able to understand when promises are handled, couldn't await just be used without async just like .then()?

To make it even more confusing, I saw people using .then() directly inside functions declared with the async keyword. Wasn't async/await supposed to be syntactic sugar for the .then().catch() approach? Why can these be combined, especially inside one another? Using .then() on the result of the async function wouldn't have been as confusing, but being inside one another makes me have even a harder time understanding this.

I really searched everywhere for an explanation on this and couldn't find an answer for this exact question. All I've found was people saying you can use both approaches because they essentially are the same thing, but when you get in the details, things aren't very clear.

So the async function always returns a promise. Inside it, await always handles promises. .then() can be chained to the await function. .then() can also be chained to the result of the async function. Same with the .catch method if we don't want to use try/catch on the await. Why is it so mixed up? Can we handle the return of async without .then()? If async/await really is syntactic sugar for .then(), why doesn't .then() also always return a promise after it resolves?

If anybody can help with some clarification, I would truly appreciate it. Thank you!

Armand Felix
  • 43
  • 1
  • 6
  • First line on [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then): "The `then()` method returns a `Promise`", so I'm not really sure what you're asking here. – Robby Cornelissen Oct 28 '21 at 05:07
  • There's about half a dozen questions here that could be better addressed by reading an introductory article. – Robby Cornelissen Oct 28 '21 at 05:12
  • The main question is why don't we need to mark an asynchronous operation with .then() and we have to do it with await. – Armand Felix Oct 28 '21 at 05:32
  • You don't need to specify that `then()` is asynchronous because you *only* call it on a promise. Therefore it is *only* ever going to be resolved asynchronously. I'd draw a parallel to `.map()` - you call it on an array but you don't need to specify a loop. It's already going to run against all values in the array. – VLAZ Oct 28 '21 at 05:51

3 Answers3

5

There's so many questions here, but one way to think about it is... Promises are not asynchronous operations, they are just a standard pattern that helps users deal with existing asynchronous operations in a predicable way.

There's nothing special about .then(). It's not a Javascript keyword, and javascript doesn't need to be 'aware' of then in any way.

Promises are a pattern. You can write a promise class yourself from scratch (I'd highly recommend this). When you use .then(), and you pass it a function, you are telling the promise class: "When the promise resolves, please call this function for me".

All of this existed before async/await. Async/await was added after to make it easier to work with promises.

I find the question if 'something is syntactic sugar' meaningless. It suggests that a language feature is not that meaningful because the same thing could be accomplished before the language feature existed. While this might be true, this is true for any programming language when compared to, say, assembly. To me the definition of 'syntax sugar' can be extended to almost anything, so it's not a useful designation.

What async/await (and generators before it) adds to the language, is that a javascript function can be interrupted and resumed later after some condition.

Lastly, .then() and .catch() always return promises. If you see a .then() function that doesn't, then it's not compatible with the Promise/A+ spec.

If you are willing to put in the work to fully understand this, I would recommend the following 2 exercises:

  • Write your own Promise class
  • Re-implement async/await using generator functions (see the co package of how this was done before async/await landed)
Evert
  • 93,428
  • 18
  • 118
  • 189
  • I will certainly take your advice and write some of my own promises to play around with them. Looked up generator functions and I get how they could be used to create the exact behaviour of async/await. I was really struggling to understand why would using .then() make Javascript wait for a promise and continue reading the next lines of code, but now it's clear. Both .then() and async/await return a promise to Javascript instantaneously after they are called and, until that promise is fulfilled, the result isn't registered back by the event loop. – Armand Felix Oct 28 '21 at 17:31
  • @ArmandFelix yeah you got it. The promise doesn't tell javascript to wait, it just provides some 'hooks' for other promises to schedule event handlers when results come in. – Evert Oct 28 '21 at 17:45
2

The purpose of async/await is to allow writing async code in a serial manner, which is mentally simpler to reason about (for some human-beings). This is useful, if you need to wait for async operation to finish before you continue with the rest of the code. For example, if you need to pass result of async operation as parameter.

Example 1

function asyncOperation1(n) { return Promise.resolve(n+1); }
function asyncOperation2(n) { return Promise.resolve(n/2); }
function asyncOperation3(n) { return Promise.resolve(n*3); }
function errorHandler(err) { console.error(err); }

function main() {
  // flow-control
  asyncOperation1(1)
    .then(asyncOperation2)
    .then(asyncOperation3)
    .then(continueAfterAsync)
    .catch(errorHandler)

  // function wrapper
  function continueAfterAsync(result) {
    console.log(result);
  }
}

main();

With async/await the code of the main function above may look like

async main() {
  try {
    console.log(
      await asyncOperation3(
        await asyncOperation2(
          await asyncOperation1(1)
        )
      )
    );
  } catch(err) {
    errorHandler(err);
  }
}

Pay attention that we don't need to rewrite async operation functions to be async function asyncOperation... to use await, but we need to declare main function as async main.

Which one is better(?) is the mater of developers's taste and previous programming languages experience. The benefit that I can see is that you don't need to wrap everything into functions and introduce additional flow-control code, leaving this complexity to JavaScript compiler.

However, there are cases, when you want to schedule some parallel tasks and you don't care which one will finish first. These kind of things would be relatively hard to do with async/await only.

Example 2

function main() {
  Promise
    .all(
      ['srv1', 'srv2', 'srv3'].map(
        srv => fetch(`${srv}.test.com/status`)
      )
    ])
    .then(
      responses => responses.some(res => res.status !== 200) ?
        console.error('some servers have problems') :
        console.log('everything is fine')
    )
    .catch(err => console.error('some servers are not reachable', err))
}

So, we see that there is a room for both .then() and await to coexist.

In some cases function may be either synchronous or asynchronous, depending on business logic (I know it's ugly, but in some cases it's unavoidable). And here we come to your main question

why don't we need to mark an asynchronous operation with .then() and we have to do it with await

In other words, why do we need async keyword at all?

Example 3

// without `async`
function checkStatus(srv) {
  if (!srv.startsWith('srv')) {
    throw new Error('An argument passed to checkStatus should start with "srv"')
  }
  return fetch(`https://${srv}.test.com/status`);
}

function main() {
  // this code will print message
  checkStatus('srv1')
    .then(res => console.log(`Status is ${res.status === 200 ? 'ok': 'error'}`))
    .catch(err => console.error(err));

  // this code will fail with
  // Uncaught TypeError: (intermediate value).then is not a function
  checkStatus('svr1')
    .then(res => console.log(`Status is ${res.status === 200 ? 'ok': 'error'}`))
    .catch(err => console.error(err));
}

However, if we define async function checkStatus, compiler will wrap the runtime error into rejected promise return value, and both parts of the main function will work.

Now let's imagine that JavaScript allows to write functions that use await without specifying async in front of them.

Example 4 (not a valid Javascript)

function checkStatus(srv) {
  if (cache[srv]) {
    data = cache[srv];
  } else {
    data = (await fetch(`https://${srv}.test.com/status`)).json();
  }
  data.x.y = 'y';
  return data;
}

What would you expect checkStatus to return? Promise, raw value or throw exception (in case data.x is undefined)?

If you say Promise, then it would be hard for developer that uses this function to understand why inside of checkStatus one can write data.x and outside of it (await data).x is required.

If raw value, the whole execution flow becomes cumbersome, and you can no longer rely on the fact that JavaScript is a single-threaded language, where no-one can change the value of the variable between two lines of code that are written in serial manner.

As you noticed, async/await is a syntactic sugar. If this syntax allows me to avoid possible runtime errors at earlier stage and keep the language backward compatible, I'm eager to pay the price of putting extra async in front of async functions.

Also, I would recommend to read the answers to JS async/await - why does await need async?

Vadim
  • 8,701
  • 4
  • 43
  • 50
  • Thank you so much! I think I finally understood the concept. Your first two examples were especially useful and the last one cleared a lot of confusion around the use of the async keyword. One last question though, what would happen if we used an async function as a callback in a normal function? Would that normal one become asyncronous automatically? – Armand Felix Oct 28 '21 at 17:24
  • @ArmandFelix, you can use async functions everywhere you can use the "normal" functions, including passing it as callback function parameter. The function that accepts async function as argument will not become async automatically. Async function is just a "normal" function which returns value of type Promise. – Vadim Oct 28 '21 at 17:30
  • But being that the callback works asyncronously in this case and the normal function would have to wait for its result, wouldn't passing it as a callback block the code until the async function resolves? – Armand Felix Oct 28 '21 at 17:37
  • You can take a look at Koa.js (the successor of Express.js) to better understand how it may be used. In Koa you pass async function as middleware callback function and use await to resolve it. https://koajs.com/ – Vadim Oct 28 '21 at 17:42
  • I mean you could pass it anywhere, I understand there's no limitations in using them. But passing it in code that was designed to execute syncronously would kind of defeat the purpose of using asynchronous code. That would only slow down execution until the promise is resolved, instead of waiting for the resolve in the callback queue, outside the normal execution of your program. – Armand Felix Oct 28 '21 at 17:43
  • Thank you! I'll take a look, actually I'm learning Express at this very moment and understand middleware fairly well. Just wanted to be extra sure I understand the fundamentals of asynchronous code before going forward as it's the main concept behind backend development. – Armand Felix Oct 28 '21 at 17:45
1

In a simple explanation, async/await is syntactic sugar (the node interpreter/compiler/optimizer will convert everything to normal Promises). The goal of this feature is turn our life easy, because the callback way/style of programming eventually lead us to make mistakes. We call this "the callback hell"

So, we can use .then() when calling functions that is decorated with async keyword, and we can await on functions that return Promise objects.

It's important for optimizations in general, that the code we write tells the compiler what we are meaning, in terms of performance. Imagine if all our codes / lines of code / instructions could be async or sync at the same time. This would lead the computers perform bad, because the task of check this during runtime is very expensive.

So that's why it's so important to us code instructions in an efficient manner.

Jone Polvora
  • 2,184
  • 1
  • 22
  • 33