Translated from javascript- Best-Practice

It’s common to hear that JavaScript is asynchronous. What does that mean? How does it affect development? How has this approach changed in recent years?

Consider the following code:

result1 = doSomething1();
result2 = doSomething2(result1);
Copy the code

Most languages process each line synchronously. The first line runs and returns the result. No matter how long it takes, the second line will run after the first line completes.

Single threaded processing

JavaScript runs on a single processing thread. When executed in a browser TAB, everything else stops. This is necessary because no changes to the page DOM occur on parallel threads; It is dangerous to redirect one thread to another URL while another thread tries to append child nodes.

This is usually not obvious to the user because each small block operation is handled quickly. For example, JavaScript detects a button click, runs the calculation, and updates the DOM. Once done, the browser is free to process the next task in the queue.

Asynchrony using callbacks

Single threading poses a problem. What happens when JavaScript calls a “slow” process, such as an Ajax request in a browser or a database operation on a server? This operation may take a few seconds – or even minutes. The browser is locked while waiting for a response. On the server, the Node.js application will not be able to handle further user requests.

The solution is asynchronous processing. Instead of waiting for completion, a process is told to call another function when the result is ready. This is called a callback and is passed as an argument to any asynchronous function. Such as:

doSomethingAsync(callback1);
console.log('finished');

// call when doSomethingAsync completes
function callback1(error) {
  if(! error) console.log('doSomethingAsync complete');
}
Copy the code

DoSomethingAsync () takes a callback function as an argument (only a reference to that function is passed, so there is almost no overhead). No matter how long doSomethingAsync() takes; All we know is that callback1() will execute at some point in the future. The console will display:

finished
doSomethingAsync complete
Copy the code

Callback Hell

Typically, callbacks can only be called by an asynchronous function. So you can use a neat anonymous inline function:

doSomethingAsync(error => {
  if(! error) console.log('doSomethingAsync complete');
});
Copy the code

By nesting callback functions, a series of two or more asynchronous calls can be made sequentially. Such as:

async1((err, res) => {
  if(! err) async2(res, (err, res) => {if(! err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.');
    });
  });
});
Copy the code

Unfortunately, this introduces callback hell – a notorious concept that even has its own web page! The code is hard to read and gets worse when error-handling logic is added.

Callback hell is relatively rare in client-side coding. If you’re making an Ajax call, updating the DOM and waiting for the animation to complete, it can go down two or three levels, but it’s usually still manageable.

The situation is different for an operating system or server process. Node.js API calls can receive file uploads, update multiple database tables, write logs, and make further API calls before sending a response.

Promises (Promises)

ES2015 (ES6) Promises Promises. Callbacks are still the underlying mechanism, but Promises offers a clearer syntax for linking asynchronous programs and making them run serially.

To use a promise-based execution procedure, asynchronous callback-based functions must be changed so that they immediately return Promise objects. This object promises to run one of two functions (passed as arguments) at some point in the future:

  • Resolve: Handle the callback function to run on successful completion, and
  • Reject: Optional callback function that runs when a fault occurs.

In the example below, the database API provides a method for connect() to accept the callback function. The external asyncDBconnect() function immediately returns a new Promise and runs any resolve() or reject() once the connection is established or fails:

const db = require('database');

// connect to database
function asyncDBconnect(param) {

  return new Promise((resolve, reject) => {

    db.connect(param, (err, connection) => {
      if (err) reject(err);
      else resolve(connection);
    });

  });

}
Copy the code

Node.js 8.0+ provides util.promisify() methods for converting callback-based functions to promise-based alternatives. There are several conditions:

  1. The callback must be passed to the asynchronous function as the last argument, and
  2. The callback function must support error handling, and its last argument is the callback function when the error occurred.

Ex. :

// Node.js: promisify fs.readFile
const
  util = require('util'),
  fs = require('fs'),
  readFileAsync = util.promisify(fs.readFile);

readFileAsync('file.txt');
Copy the code

Various client libraries also offer promisify options, but you can create a few yourself:

// promisify a callback function passed as the last parameter
// the callback function must accept (err, data) parameters
function promisify(fn) {
  return function() {
      returnnew Promise( (resolve, reject) => fn( ... Array.from(arguments), (err, data) => err ? reject(err) : resolve(data) ) ); } } // examplefunction wait(time, callback) {
  setTimeout(() => { callback(null, 'done'); }, time);
}

const asyncWait = promisify(wait);

ayscWait(1000);
Copy the code

Asynchronous link

Anything that returns a Promise can initiate a series of asynchronous function calls defined in the.then() method. Each passes the result resolve from the previous one:

asyncDBconnect('http://localhost:1234')
  .then(asyncGetSession)      // passed result of asyncDBconnect
  .then(asyncGetUser)         // passed result of asyncGetSession
  .then(asyncLogAccess)       // passed result of asyncGetUser
  .then(result => {           // non-asynchronous function
    console.log('complete');  //   (passed result of asyncLogAccess)
    return result;            //   (result passed to next .then())
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });
Copy the code

Synchronization can also be performed as a.then() block. The returned value is passed to the next.then() (if any).

The.catch() method defines the function to call when reject fires any of the previous functions. At this point,.then() runs other methods. You can use.catch() in various ways throughout the chain to catch different errors.

ES2018 introduces a.finally() method that allows any final logic to run regardless of the result – for example, cleaning up, closing database connections, etc. Currently only Chrome and Firefox is supported, but TECH Committee 39 has released it. Finally () polyfill.

function doSomething() {
  doSomething1()
  .then(doSomething2)
  .then(doSomething3)
  .catch(err => {
    console.log(err);
  })
  .finally(() => {
    // tidy-up here!
  });
}
Copy the code

Make multiple asynchronous calls using promise.all ()

The promise.then () methods run asynchronous functions one by one. If the order does not matter – for example, initialize unrelated components – start all asynchronous functions at the same time and complete faster resolve when the last (slowest) function runs.

This can be done by implementing promise.all (). It takes a series of functions and returns another Promise. Such as:

Promise.all([ async1, async2, async3 ]) .then(values => { // array of resolved values console.log(values); / / (in same order as function array)
    return values;
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });
Copy the code

Promise.all() terminates reject immediately if any of the asynchronous functions are called.

Multiple asynchronous calls using promise.race ()

Promise.race() is similar to promise.all (), except that it is resolved or rejected immediately after the first Promise is resolved or rejected. Only the fastest promise-based asynchronous function can do this:

Promise.race([ async1, async2, async3 ])
  .then(value => {            // single value
    console.log(value);
    return value;
  })
  .catch(err => {             // called on any reject
    console.log('error', err);
  });
Copy the code

A hopeful future?

Promises reduce back to hell but introduce problems of their own.

Tutorials often fail to mention that the entire Promise chain is asynchronous. Use a series of promises that end up either returning their own promises or running any function of the callback function. Then (),.catch() or.finally() methods.

I have to admit: Promises have left me confused for a long time. Syntax often looks more complex than callbacks, with lots of errors, and debugging can be problematic. But learning the basics is crucial.

Further Promises resources:

  • MDN Promise document
  • JavaScript Promises: Introduction
  • JavaScript Promise…… In sinister detail
  • Promises Asynchronous programming

Async and Await

Promises may still be difficult to use, so ES2017 introduces Async and await. While it may just be syntactic sugar, it makes promises so much sweeter that you can avoid the.then() chain altogether. Consider the following promise-based example:

function connect() {

  return new Promise((resolve, reject) => {

    asyncDBconnect('http://localhost:1234')
      .then(asyncGetSession)
      .then(asyncGetUser)
      .then(asyncLogAccess)
      .then(result => resolve(result))
      .catch(err => reject(err))

  });
}

// run connect (self-executing function)
(() => {
  connect();
    .then(result => console.log(result))
    .catch(err => console.log(err))
})();
Copy the code

Rewrite with async/await:

  1. External functions must begin with an async statement, and
  2. You must precede the asynchronous promise-based calling function with await to ensure that the next piece of code is executed after completion.
async function connect() {

  try {
    const
      connection = await asyncDBconnect('http://localhost:1234'),
      session = await asyncGetSession(connection),
      user = await asyncGetUser(session),
      log = await asyncLogAccess(user);

    return log;
  }
  catch (e) {
    console.log('error', err);
    return null;
  }

}

// run connect (self-executing async function) (async () => { await connect(); }) ();Copy the code

Await effectively makes each call look as if it is synchronous, rather than blocking a single thread of JavaScript processing. In addition, async functions always return a Promise, so they can be called by other async functions.

Async/await code may not be shorter, but has considerable benefits:

  1. The syntax is clearer. Fewer parentheses, fewer errors.
  2. Debugging is easier. Breakpoints can be set on any await statement.
  3. Error handling is better. Try /catch blocks can be used in the same way as synchronized code.
  4. The support is good. It is implemented in all browsers except IE and Opera Mini and Node 7.6+.

That said, not everything is perfect……

Promises, Promises

Async/await still depends on Promises and ultimately on callbacks. You need to understand how Promises work and that Promises don’t have the same syntax as promise.all () and promise.race (). It’s easy to forget promise.all (), but it’s more effective than using a series of unrelated await commands.

Asynchronous wait in a synchronous loop

At some point, you might try to call asynchronous functions within a synchronous loop. Such as:

async function process(array) {
  for (let i of array) {
    await doSomething(i); }}Copy the code

It won’t work. Nor will this:

async function process(array) {
  array.forEach(async i => {
    await doSomething(i);
  });
}
Copy the code

Loops themselves remain synchronous and always complete before their internal asynchronous operations.

ES2018 introduces asynchronous iterators, which are the same as regular iterators, except that the next() method returns a Promise. Therefore, the await keyword can be associated with for… The of loop is used together to run asynchronous operations serially. Such as:

async function process(array) {
  for await (let i of array) {
    doSomething(i); }}Copy the code

However, before implementing asynchronous iterators, it is best to convert arrays into arrays of async functions via map and run them through promise.all (). Such as:

const
  todo = ['a'.'b'.'c'],
  alltodo = todo.map(async (v, i) => {
    console.log('iteration', i);
    await processSomething(v);
});

await Promise.all(alltodo);
Copy the code

This has the benefit of running tasks in parallel, but you can’t pass the results of one iteration to another, and mapping large arrays can have a significant performance cost.

Try/catch ugly

If you omit to wrap an await with a try/ catch block, async will exit when an error occurs. If you have a long set of asynchronous await commands, you may need multiple try/ catch blocks.

An alternative is to use higher-order functions to catch errors, so try/ catch blocks become unnecessary:

async function connect() {

  const
    connection = await asyncDBconnect('http://localhost:1234'),
    session = await asyncGetSession(connection),
    user = await asyncGetUser(session),
    log = await asyncLogAccess(user);

  return true;
}
// higher-order function to catch errors
function catchErrors(fn) {
  return function(... args) {returnfn(... args).catch(err => { console.log('ERROR', err); }); } } (async () => { await catchErrors(connect)(); }) ();Copy the code

However, this approach may not be practical in cases where the application must handle different errors in different ways.

Despite some pitfalls, async/ await is an elegant complement to JavaScript. More resources:

  • MDN asynchrony and waits
  • Asynchronous feature – Make Promises Friendly
  • TC39 asynchronous function specification
  • Use asynchronous functions to simplify asynchronous coding

JavaScript trip

Asynchronous programming is a challenge that cannot be avoided in JavaScript. Callbacks are essential in most applications, but it’s easy to get bogged down in deeply nested functions.

Promises abstract callback, but has a lot of syntax traps. Converting existing functionality can be a chore, and the.then() chain still looks messy.

Fortunately, async/ await provides clarity. The code appears to be synchronous, but it cannot monopolize a single processing thread. It will change the way you write JavaScript and even make you appreciate Promises – if you didn’t have one before!