TECHY360
Everything You Need To Know About Tech

Understanding Async / Await in JavaScript with examples

0 9

A callback is not something intricate or special, but simply a function whose call is postponed indefinitely. Due to the asynchronous nature of JavaScript, callbacks were needed wherever results could not be obtained immediately.

Below is an example of an asynchronous file reading on Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

Problems begin when you need to perform several asynchronous operations. Just imagine a similar scenario:

  • A query is executed in the database for a certain user Arfat. It is necessary to read his field profile_img_urland download the corresponding image from the server someServer.com.
  • After downloading the image you need to convert it, let’s say from PNG to JPEG.
  • In case of a successful conversion, you need to send an email to the user’s mail.
  • This event should be recorded in the file transformations.log and specify the date.
queryDatabase({ username: 'Arfat'}, (err, user) => {
  // Error handling when querying in the database
  const image_url = user.profile_img_url;
  getImageByURL('someServer.com/q=${image_url}', (err, image) => {
    // Error handling image acquisition
    transformImage(image, (err, transformedImage) => {
      // Conversion Error Handling
      sendEmail(user.email, (err) => {
        // Error handling of sending by mail
        logTaskInFile('File Conversion and Mailing', (err) 
          // Log Error Handling
        })
      })
    })
  })
})

Note the callback nesting and the pyramid })at the end. Such cases are called Callback Hell or Pyramid of Doom. Here are the main disadvantages:

  • This code is difficult to read.
  • In this code, it is difficult to handle errors and at the same time preserve its “quality”.

To solve this problem in JavaScript promises were invented (eng. Promises ). Now the deep nesting of callbacks can be replaced by the keyword then:

queryDatabase({ username: 'Arfat'})
  .then((user) => {
    const image_url = user.profile_img_url;
    return getImageByURL('someServer.com/q=${image_url}') 
      .then(image => transformImage(image))
      .then(() => sendEmail(user.email))
})
.then(() => logTaskInFile('...'))
.catch(() => handleErrors()) // Error processing

The code began to be read from top to bottom, and not from left to right, as was the case with callbacks. This is a plus to readability. However, the promises have their own problems:

  • Still need to work with a bunch .then.
  • Instead of the usual, try/catchyou should use .catchto handle all errors.
  • Working with several promises in a cycle is not always intuitive and sometimes difficult.

As a demonstration of the last item try this task:

Suppose you have a loop forthat outputs a sequence of numbers from 0 to 10 at a random interval (from 0 to n seconds). Using promises, you need to change the cycle so that the numbers are displayed in a strict sequence from 0 to 10. For example, if the zero output takes 6 seconds and the units 2 seconds, then the unit must wait for the zero output and only then start its countdown (to observe the sequence).

Needless to say that in solving this problem it is impossible to use construction async/awaitor a .sortfunction? The decision will be in the end.


Async features

The addition of async functions in ES2017 (ES8) made working with promises easier.

  • It is important to note that async functions run over promises.
  • These functions are not fundamentally different concepts.
  • Async functions were conceived as an alternative to code using promises.
  • Using the async / await construct, you can completely avoid the use of promise chains.
  • With the help of async functions, it is possible to organize work with asynchronous code in a synchronous style.

As you can see, the knowledge of promises is still necessary to understand how async / await works.

Syntax

The syntax consists of two keywords: asyncand await. The first makes the function asynchronous. It is in such functions that usage is permitted await. Use awaitin any other case will cause an error.

// In the function declaration
async function myFn() {
  // await ...
}

// In switch function
const myFn = async () => {
  // await ...
}

function myFn() {
  // await fn(); (syntax error, since no async)
}

Notice what is asyncinserted at the beginning of the function declaration, and in the case of the arrow function, between the sign =and the brackets.

Async functions can be placed in an object as methods or simply used in a class declaration.

// As an object method
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}

// In the classroom itself
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

Note Class constructors and getters/setters cannot be asynchronous.

Semantics and execution rules

Async functions are similar to regular functions in JavaScript, with the exception of a few things:

Async functions always return promises

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

The function fnreturns a string 'hello'. Since this is an asynchronous function, the string value is wrapped in a promise (using a constructor).

The code above can be rewritten without using async:

function fn() {
  return Promise.resolve('hello');
}
fn().then(console.log);
// hello

In this case, instead async, the code manually returns a promise.

The body of the asynchronous function is always wrapped in a new promise.

If the return value is a primitive, the async function returns that value wrapped in promise. But if the return value is the object of promise, its solution is returned in the new promise.

// In the case of a primitive type of value
const p = Promise.resolve('hello')
p instanceof Promise; 
// true

// p returns as it is

Promise.resolve(p) === p; 
// true

What happens when an error occurs inside an asynchronous function?

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

foo().catch(console.log);

If the error is not processed, it foo()will return a promise with a reject. In such a case, the error Promise.resolvewill return instead Promise.reject.

Related Posts
1 of 6

The essence of async functions is that whatever you return, you will always receive a promise at the output.

Asynchronous functions are suspended with each awaits expression.

awaitaffects expressions. If the expression is a promise, then the async function will be suspended until the promise is executed. If the expression is not a promise, then it is converted into promise after Promise.resolveand then it ends.

// Delay function
// with the return of a random number
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};

async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
  
  return a + b * c;
}

// Вызов fn
fn().then(console.log);

How does the fnfunction work ?

  1. After calling the fnfunction, the first line is converted from const a = await 9;to const a = await Promise.resolve(9);.
  2. After use await, the execution of the function is suspended until ait receives its value (in this case it is 9).
  3. delayAndGetRandom(1000)pauses the execution of the fnfunction until it completes itself (after 1 second). This, in fact, can be called a stop fnfunction for 1 second.
  4. Also delayAndGetRandom(1000)through resolveit returns a random value that is assigned to the variable b.
  5. The variable ccase is identical to the variable case a. After that, there is again a pause for 1 second, but now delayAndGetRandom(1000)it returns nothing, since this is not required.
  6. At the end of these values ​​are calculated by the formula a + b * c. The result is wrapped in promis with help Promise.resolveand returned by the function.

Note: If such pauses remind you of generators in ES6, then there are reasons for this.

The solution to the problem

Here is the solution to the problem posed at the beginning of the article, using async/await.

async function finishMyTask() {
  try {
    const user = await queryDatabase({ username: 'Arfat' }); 
    const image_url = user.profile_img_url;
    const image = await getImageByURL('someServer.com/q=${image_url}); 
    const transformedlmage = await transformImage(image); 
    await sendEmail(user.email); 
    await logTaskInFile(' ... ');
  } catch(err) {
    // Handling all errors
  }
}

The function finishMyTaskis used awaitto wait for the results of such operations as queryDatabasesendEmaillogTaskInFileand so on. D. If we compare this solution with the solutions used Promis, you pay attention to their similarities. However, the version with async / await simplifies the syntactic complexity. In this method, there is a heap callbacks and chains .then.catch.

Here is the solution with the output numbers. There are two ways:

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));

// Solution # 1 (using a for loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});

// Solution # 2 (using recursion)

const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {

    if (i === 10) {
      return undefined;
    }

    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

Using async functions, the solution of the problem is simplified to disgrace:

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Error processing

As mentioned above, the raw errors are wrapped in an unsuccessful ( rejected ) promise. But in async functions, you can still use a construct try-catchfor synchronous error handling.

async function canRejectOrReturn() {
  // Waiting for a second
  await new Promise(res => setTimeout(res, 1000));
// in 50% of the case
  if (Math.random() > 0.5) {
    throw new Error('Sorry, the number is more than necessary.')
  }

return 'The number came up';
}

canRejectOrReturn()– this is an asynchronous function that will successfully complete with 'The number came up', or fail with Error('Sorry, the number is more than necessary.').

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'Error processed';
  }
}

Since the code above is expected to execute canRejectOrReturn, its own failure will cause the block to execute catch. Therefore, the function foowill end either with undefined( since trynothing is returned in the block ), or with 'Error processed’. Therefore, this function will have no failure, since the try-catch the block will handle the error of the function itself foo.

Here is another example:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'Error processed';
  }
}

Note that in the code above from fooreturns (no waiting) canRejectOrReturnfoowill end either with '
the number came up 'or with the reject. The block will never be executed.Error('Sorry, the number is more than necessary.')catch

This is due to the fact that the foopromise returns, which is transmitted from canRehectOrReturn. Therefore, the solution of the function foobecomes the solution canRejectOrReturn. This code can be presented in just two lines:

try {
    const promise = canRejectOrReturn();
    return promise;
}

Here’s what happens if you use awaitit returnall at once:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'Error processed';
  }
}

The above code foowill successfully complete both with 'the number came up'and with 'Error processed'. There will be no rejects in this code. But unlike one of the examples above, it fooends with a value canRejectOrReturn, not c undefined.

You can see for yourself by removing the line return await canRejectOrReturn():

try {
    const value  = await canRejectOrReturn();
    return value;
}
// ...

Popular bugs and pitfalls

Due to complex manipulations with promises and async / await concepts, you may encounter various subtleties that can lead to errors.

Do not forget await

A common mistake is that the keyword is forgotten before the promise await:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'Treatment';
  }
}

Please note here is used neither await, nor return. The function foo will always terminate with undefined(no 1-second delay). However, the promise will be executed. If the promise produces an error or a rejection, it will be called UnhandledPromiseRejectionWarning.

async functions in callbacks

async functions are often used in. mapor . filteras callbacks. Here is an example – let’s say there is a function fetchPublicReposCount(username)that returns the number of open repositories on GitHub. There are 3 users whose indicators you need to take. The following code is used:

const url = 'https://api.github.com/users';

// Gets the number of open repositories
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

And in order to get the number of user repositories ( ['ArfatSalman', 'octocat', 'norvig']), the code should look something like this:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];

const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Note the word awaitin the function callback .map. One would expect that the variable countswould contain a number – the number of repositories. But as mentioned earlier, all async functions return promises. Therefore, it countswill be an array of promises. .mapcalls an anonymous callback for each user.

Using await too consistently

Suppose there is such a code:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

The countnumber of repositories is placed in the variable, then this number is added to the array counts. The problem with this code is that until the data of the first user comes from the server, all subsequent users will be waiting. It turns out that only one user is processed at a time.

If the processing of one user will take 300 ms, then all users will take almost a second. In this case, the time spent will linearly depend on the number of users. Since obtaining the number of repositories does not depend on each other, it is possible to parallelize these processes. Then users will be processed simultaneously and not sequentially. To do this, you need .mapand Promise.all.

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.allat the entrance, it receives an array of promises and returns promises. The returned promise is completed after all promises have been completed in the array or on the first reject. Perhaps all these promises will not run strictly simultaneously. To achieve strict parallelism, take a look at the p-map. And if you want the async functions to be more adaptive, look at the Async Iterators.

Get real time updates directly on you device, subscribe now.

Comments
Loading...

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More