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

How to Stop Using Callbacks and Start Living

DZone's Guide to

How to Stop Using Callbacks and Start Living

In this article, we look at JavaScript and how it is not difficult to write well structured, easy to understand asynchronous code without using any third-party libraries.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Image title

JavaScript has two major ways of dealing with asynchronous tasks - callbacks and Promises. In general, Promises are considered easier to use and to maintain than callbacks. But, in reality, even Promises alone won’t make you happy. Asynchronous code may still be quite difficult to read and to understand. Therefore, third-party libraries, like co, provide the means to write synchronous-like asynchronous code.

I personally prefer everything in the world to be as clear and beautiful as redux-saga. But not everybody is lucky to work with React and Redux to be able to use sagas. In this article, I will show that in modern JavaScript it is not difficult to write well structured and easy to understand asynchronous code without using any third-party libraries.

Callback Hell

Let’s start with an example. Say we have an object that can read some data from a stream and this object uses an event emitter to notify everyone interested of the events. The events are startdata, stop and, to make things a bit more complicated, pause.

So we want to catch the start event on which we would like start getting and storing data while listening to the data event. And on the stop event we need to perform some data processing. On the pause event, we stop waiting for the next data event and wait for start instead to continue getting and storing data.

Here is the code:

let data = '';

const handleStart = () => {
   streamReader.removeAllListeners('pause', handlePause);

   streamReader.on('data', (chunk, err) => {
       if (err) {
           console.error(err);
           streamReader.removeAllListeners('data');
           streamReader.removeAllListeners('pause');
           return;
       }

       data += chunk;
   })
}

const handleStop = () => {
   streamReader.removeAllListeners('data');
   streamReader.removeAllListeners('pause');
   streamReader.removeAllListeners('stop');

   processData(data, (err, result) => {
       if (err) {
           console.error(err);
           return;
       }

       storeResult(result, () => {
           console.log('Stored')
       })
   });
}

const handlePause = () => {
   streamReader.removeAllListeners('data');
   streamReader.on('start', handleStart);
}

streamReader.once('start', handleStart);
streamReader.on('stop', handleStop)
streamReader.on('pause', handlePause);

Here we have a bunch of event listeners and event handlers that implement the flow described above. Also there are some functions called processData and storeData which perform some asynchronous actions and call a callback when finished.

What is wrong with this code? Well… I think, it’s a complete nightmare. First of all, there is a global variable, data, that's impossible to get rid of. Also, I mentioned a flow above, but there is no flow in the code. It is very difficult to comprehend the sequence of actions and therefore it is very difficult to debug. People don’t call it ‘callback hell’ for nothing.

Way Out

The good thing about the asynchronous callbacks in JavaScript is that you don’t have to ever use them if you don’t want to. Any callback may be turned into a Promise. The simplest example would look like this:

const processDataPromise = new Promise((resolve, reject) => {
   processData(data, (err, result) => {
       if (err) reject(err);
       resolve(result);
   });
})

Or a more general solution:

function promisify(f, context, isEvent) {
   const ctx = context || this;
   return function () {
       return new Promise((resolve, reject) => {
           f.call(ctx, ...arguments, (...args) => {
               const err = arguments ? args.find((a) => a instanceof Error) : null;
               if (err) {
                   reject(err);
               } else {
                   if (isEvent) {
                       resolve({
                           type: arguments[0],
                           cbArgs: [...args],
                       });
                   } else {
                       resolve([...args]);
                   }
               }
           })
       });
   }
}

The general solution is not immediately clear off course, so let me explain.

The function promisify takes an asynchronous function as the first argument and returns a function that takes all the same parameters as the original one except for the callback. When this returned function is called, it returns a Promise. The original function is called inside the Promise which is resolved when the original function calls the callback. If the original function has a context (the context argument of promisify), it is bound to it when called inside the Promise. If the original function is just an ordinary asynchronous function, we resolve the Promise with the callback's arguments. If it is an event listener (isEvent = true), we return both the event type and the callback arguments. And if the callback is called with and error, the Promise gets rejected.

The application of promisify looks like this:

const processDataPromise = promisify(processData);
const storeResultPromise = promisify(storeResult);
const onEventPromise = promisify(emitter.once, emitter, true);

And the Promise may be used this way:

processDataPromise(data).then(([err, processedData]) => {
/* do something with the data*/
})

But there is a better way.

Saga-Like Heaven

And we do need a better way here, because it is pretty much impossible to squash a flow like the one described above into a Promise chain.

The better way is a JavaScript async function and here is another implementation of the same flow:

async function readStream(streamReader, initialData) {
const processDataPromise = promisify(processData);
const storeResultPromise = promisify(storeResult);
const onEventPromise = promisify(emitter.once, emitter, true);

   await onEventPromise('start');
   let data = initialData || '';

   while (true) {
       try {
           const event = await Promise.race([
               onEventPromise('data'),
               onEventPromise('stop'),
               onEventPromise('pause'),
           ]);

           const {type} = event;

           if (type === 'data') {
               const [chunk] = event.cbArgs;
               data += chunk;
           }

           if (type === 'pause') {
               func(streamReader, data);
               break;
           }

           if (type === 'stop') {
               const [err, processedData] = await processDataPromise(data);
               await storeResultPromise(processedData);
               return processedData;
           }
       } catch (err) {
           handleError(err);
           return;
       }
   }
}

Well, what is good about this implementation? To start with, it just looks prettier. Secondly and more importantly, there is a flow in this code. It is almost like flowchart where you can trace the whole sequence with every loop and every branch step-by-step. It does look like a saga I mentioned in the beginning, but there is no need to know anything about redux-saga to write code like this. More importantly, it is code you can live with.

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
javascript ,callback hell ,tutorial ,web dev ,react-redux

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}