Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Asynchronous Programming: A Reply

DZone's Guide to

Asynchronous Programming: A Reply

In this post, we take a look at asynchronous in Node.js, and a few things to avoid in order to make your JS as clean as possible.

· Web Dev Zone
Free Resource

Get deep insight into Node.js applications with real-time metrics, CPU profiling, and heap snapshots with N|Solid from NodeSource. Learn more.

Recently, Jesse Warden published an article about Asynchronous Programming on DZone which also can be found on his blog. Actually, there are a couple of misleading statements mostly due to the incorrect modification of his examples with JavaScript Promises and async/wait modifications.

A better example of how JavaScript behaves with Node.js is described in "The Node.js Event Loop, Timers, and process.nextTick()." Most of this article applies to browser-hosted JavaScript as well.

Jesse's first pre-ECMAScript2015 conformant example looks like this:

fast = () => console.log('1');
slow = callback => {
  setTimeout(()=> {
    console.log('2');
    callback();
  }, 1000);
};

fast();
slow(()=> console.log('3'));
console.log('4');

If you are familiar with JavaScript, you will not be surprised about the output of this code snippet being the numbers 1 through 4 written in the sequence 1, 4, 2, 3: the timeout event is only handled after all the synchronous code has terminated execution. 

Later in his article, Jesse removes the call to setTimeout(), and his code now looks like this:

fast = () => console.log('1');
slow = callback => {
    console.log('2');
    callback();
};
fast();
slow(() => console.log('3'));
console.log('4');

In this case, we get the sequence 1, 2, 3, 4. Jesse states: "Depending on 'how fast' your code runs, especially when unit testing with mocks, you now have a race condition." This is wrong. This code is completely synchronous and behaves as it should - namely by sequentially executing its statements. The call to slow() is executed immediately which means the two statements inside the lambda block are executed before the call to fast(). There is no race condition at all. Maybe his mocks introduced asynchronicity, which led him to his conclusion.

Here is Jesse's version of the example which uses promises:

fast = () => console.log('1');
slow = () => new Promise(success => {
    setTimeout(()=> {
        console.log('2');
        success();
    }, 100);
});

fast();
slow().then(()=> console.log('3'));
console.log('4');

Actually, what this code does is likely not to be what Jesse wants it to, namely, bringing code execution in an order which produces the output 1, 2, 3, 4. What it does not do is to serialize the output. Indeed, outputting 3 does not happen before the timeout event has occurred and was handled. This is the behavior which is implemented with this promise. However, the call to slow() only creates the promise and initiates the action by calling the timeout() function without waiting for the timeout event to occur. So the next action is the call to then() which places another callback onto the stack of actions to be executed after the timeout event has been handled successfully. Both the event handling and the action stack are executed only after all the synchronous code has finished executing - and perhaps some other stuff like the keyboard or another I/O was processed. Therefore, the output of the sequence 1, 4, 2, 3 is exactly what one should expect. So Jesse ends up with possible race conditions due to his inappropriate usage of promises.

As you may have noticed, this version does not take any parameters, so there is no way to pass a callback to it which will be executed when the timeout event occurs.

An - almost - correct implementation would look like this:

function fast() {
  console.log('1');
}

function slow(callback) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('2');
      callback();
      resolve();
    }, 1000);
  });
}

function doStuff() {
  fast();
  slow(() => console.log('3')).then(() => console.log('4'));
}

doStuff();

This indeed delivers the desired sequence of 1, 2, 3, 4.

Here is the same code when using async/wait:

function fast() {
  console.log('1');
}

function slow(callback) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('2');
      callback();
      resolve();
    }, 1000);
  });
}

async function doStuff() {
  fast();
  await slow(() => console.log('3'));
  console.log('4');
}

doStuff();

I call this code almost correct, however, since it does not allow you to pass any parameters to the callback or to handle possible return values of it. Here's a much better version then is this one which also adds exception handling:

function fast() {
  console.log('1');
}

function slow(callback, …args) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('2');
      try {
        let retVal = callback(…args);
        resolve(retVal);
      } catch(e) {
        let errObj = { err: e, args: […args] };
        reject(errObj);
      }
    }, 1000);
  });
}

function doStuffWithPromises() {
  fast();
  slow((arg) => { return arg; }, '3').
  then((res) => { console.log(res); }).
  then(() => { console.log('4'); }).
  then(() => {
    fast();
    slow((arg) => { throw('Oops!'); }, '3').
    then((res) => { console.log(res); }).
    then(() => { console.log('4'); }).
    catch((e) => {
      console.log('Oh horror!');
      console.log('An error occurred:');
      console.log('Error:', e.err);
      console.log('Arguments:', e.args);
    });
  });
}

async function doStuffAsynchronously() {
  fast();
  var retVal = await slow((arg) => { return arg; }, '3');
  console.log(retVal);
  console.log('4');
  fast();
  try {
    retVal = await slow((arg) => { throw('Oops!'); }, '3');
    console.log(retVal);
    console.log('4');
  } catch(e) {
    console.log('Oh horror!');
    console.log('An error occurred:');
    console.log('Error:', e.err);
    console.log('Arguments:', e.args);
  }
}

doStuffWithPromises();
doStuffAsynchronously();

As you can see, the return value of the first callback reappears as input to the second callback passed to the first call of the then() method, in case you prefer working with promises. Otherwise, this value is returned by the call to await slow(), which makes it look much like a synchronous call. Furthermore, you can separate general (reusable) exception handling (which is implemented here during the creation of the promise, that is as part of slow() itself) from the exception handling specific to the particular use case when slow() is called. This nicely illustrates the principle that you should not try to handle exceptions completely in a function when you don't have enough information to do it in a meaningful way. Just add a little bit of contextual information if this makes sense, and rethrow. Somewhere further up in the call stack, exceptions can be handled as required by the needs of the business.

With await, exception handling looks quite natural and similar to that in synchronous code, and debugging code is much easier in this case since you do not need to step through a bunch of callbacks like you need to when you use promises directly. This is why I prefer working with await.

As a summary, I'd like to state that when using promises correctly, you can avoid race conditions altogether.

Node.js application metrics sent directly to any statsd-compliant system. Get N|Solid

Topics:
web dev ,asynchronous code ,javascript ,node.js

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}