Skip to content
Advertisement

Why doesn’t .then() need the async keyword when used (similar to await)? How does Javascript know it’s an asynchronous operation?

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!

Advertisement

Answer

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?

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement