As we already learned, in nonblocking environments, such as Node.js, most of the processes are asynchronous. A request comes to our code, and our server starts processing it but at the same time continues to accept new requests. For example, the following is a simple file reading:
The readFile
method accepts two parameters. The first one is a path to the file we want to read, and the second one is a function that will be called when the operation finishes. The callback is fired even if the reading fails. Additionally, as everything can be done via that asynchronous matter, we may end up with a very long callback chain. There is a term for that—callback hell. To elucidate the problem, we will extend the previous example and do some operations with the file's content. In the following code, we are nesting several asynchronous operations:
As you can see, our code looks bad. It's difficult to read and follow. There are a dozen instruments that can help us to avoid such situations. However, we can fix the problem ourselves. The very first step to do is to spot the issue. If we have more than four or five nested callbacks, then we definitely should refactor our code. There is something very simple, which normally helps, that makes the code shallow. The previous code could be translated to a more friendly and readable format. For example, see the following code:
Most of the callbacks are just defined separately. It is clear what is going on because the functions have descriptive names. However, in more complex situations, this technique may not work because you will need to define a lot of methods. If that's the case, then it is good to combine the functions in an external module. The previous example can be transformed to a module that accepts the name of a file and the callback function. The module is as follows:
You still have a callback, but it looks like the helper methods are hidden and only the main functionality is visible.
Another popular instrument for dealing with asynchronous code is the promises paradigm. We already talked about events in JavaScript, and the promises are something similar to them. We are still waiting for something to happen and pass a callback. We can say that the promises represent a value that is not available at the moment but will be available in the future. The syntax of promises makes the asynchronous code look synchronous. Let's see an example where we have a simple module that loads a Twitter feed. The example is as follows:
We attached a listener for the loaded
event and called the getData
method, which connects to Twitter and fetches the information. The following code is what the same example will look like if the TwitterFeed
class supports promises:
The promise
object represents our data. The first function, which is sent to the then
method, is called when the promise
object succeeds. Note that the callbacks are registered after calling the getData
method. This means that we are not rigid to actual process of getting the data. We are not interested in when the action occurs. We only care when it finishes and what its result is. We can spot a few differences from the event-based implementation. They are as follows:
- There is a separate function for error handling.
- The
getData
method can be called before calling the then
method. However, the same thing is not possible with events. We need to attach the listeners before running the logic. Otherwise, if our task is synchronous, the event may be dispatched before our listener attachment. - The promise method can only succeed or fail once, while one specific event may be fired multiple times and its handlers can be called multiple times.
The promises get really handy when we chain them. To elucidate this, we will use the same example and save the tweets to a database with the following code:
So, if our successful callback returns a new promise, we can use then
for the second time. Also, we have the possibility to set only one error handler. The catch
method at the end is fired if some of the promises are rejected.
There are four states of every promise, and we should mention them here because it's a terminology that is widely used. A promise could be in any of the following states:
- Fulfilled: A promise is in the fulfilled state when the action related to the promise succeeds
- Rejected: A promise is in the rejected state when the action related to the promise fails
- Pending: A promise is in the pending state if it hasn't been fulfilled or rejected yet
- Settled: A promise is in a settled state when it has been fulfilled or rejected
The asynchronous nature of JavaScript makes our coding really interesting. However, it could sometimes lead to a lot of problems. Here is a wrap up of the discussed ideas to deal with the issues:
- Try to use more functions instead of closures
- Avoid the pyramid-looking code by removing the closures and defining top-level functions
- Use events
- Use promises