Most asynchronous operations like web requests can cause an error. Thus, let’s spend a minute reviewing the basics of the code flow.
Like with C# and Java, you place the “risky” code into a try-block. If that code causes an error (exception), you handle it in a catch-block. Of course, the exception must be triggered at some point. This is done with the throw statement.
The following is a basic example. It checks if the weight is positive. You could use it in a healthcare app that needs to reject obviously wrong measurements containing negative weights.
In the example shown above, the code would cause an error. The variable weight was initialized with -12.2. In the if statement within the try/catch block, weight is checked and any value below 0 is considered as invalid. The code uses the “throw” statement to explicitly cause an error. Execution flow immediately jumps to the catch-block and prints the error message to the console. The “Saving…” console print is not executed. Instead, the console reads:
Weight can not be less than 0
If you would assign a positive value to the weight, the execution flow is as follows:
The if statement doesn’t execute, the console prints the success message and then jumps over the catch-block.
The downside is that the web worker thread doesn’t have access to the DOM. For example, to update the user interface with the current loading progress, your worker sends a message to the UI thread, which then updates the UI. That’s a little code overhead to consider.
How do callbacks work? They are the “traditional” way of responding to finished asynchronous requests. The result either contains an error, or the returned data you expected.
To test this approach, create a new Node.js project through:
Next, install the request module. It makes HTTP requests easier:
npm install request
Finally, create a new file with your favorite code editor and call it index.js. Enter the following code – it downloads and shows a website through HTTPS:
The second parameter of the request call defines the callback function. In this case, it’s an anonymous function, which doesn’t have its own method name.
First, the function checks for errors. In case the request was successful, it prints the received contents from the web server.
Note the execution flow: First, you see the console output in the last line of our code with “Starting…”. Only after the asynchronous HTTPS request finishes, the app starts printing the body of the received page.
Alternatively, you could of course use a separate, named function instead of the anonymous function. The code stays the same, but it would give you more options to organize the code or to re-use the callback function for other requests.
The downside of this approach? While the code looks nice & clean, in many real-life scenarios you need to chain different steps together. For example, once the login is complete, you save something to a database, then fetch the latest news for the user, …
With all these call-backs, your code will become difficult to read and to maintain. Especially error handling can become hard if you do it separately for each callback. The website callbackhell.com has a good description and workarounds.
In this example, you see that each call-back handles the errors individually.
Also note the last 5 lines. You’ll see the closing of deeply nested functions. It’s already tricky for just two requests. Now imagine what it looks like when you chain more requests together.
Fortunately, ES 2015 / 6+ introduces a good alternative, called Promises. Instead of callbacks, you use two functions:
- then(): successful request. Can also be chained to handle one request after each other.
- catch(): failure. You can also use a single catch for all chained promises.
Let’s try this. Node.js has another extension module called request-promise-native that adds support for promises to HTTP requests using the native functionality of ES 2015. Install it through:
npm install request-promise-native
The following code example is like the previous from “Callback hell”. But it’s a lot easier to read and has central error handling. If an error occurs for one of the requests, it immediately stops further execution of the promise chain.
Let’s look at the execution flow. Every .then() is handled after each other. This example also uses anonymous functions. Of course, it would also work to put the code to separate named functions instead.
Async / Await
While promises are a good concept, they also introduce a considerable amount of code overhead. After all, they still need an extra function for each asynchronous response.
Fortunately, ES 2017+ and Node.js 7.6+ support the async / await concept, which C# developers know and love. As such, it’s also supported in Node.js 8, which is the current Long Term Support (LTS) variant.
Example: Web Requests with Async / Await
Let’s see how the same example works with Async / Await. You don’t need any different extension modules, as these keywords are a language feature. Therefore, we’re using the same module as in the last example: request-promise-native.
The example has the same functionality as before. But everything’s happening in a single function. That makes the code a lot shorter and easier to read. It’s also closer to the “gut feeling” of how code should look like. After all, asynchronous code is so common today. Why should I create countless individual functions just because something I’d like to do is asynchronous?
In the image above, you see the visualized execution flow. It starts with calling the processData() function. Note that you’re only allowed to use the await keyword in functions marked with async.
The actual web request doesn’t need the second argument for the callback anymore. Instead, the await keyword before the function call makes the execution within this function “pause” until the asynchronous call completed.
Only then, execution continues. Our example code checks the response of the first request and starts another request.
Error handling is also centralized with the familiar try / catch concept introduced at the beginning. The block surrounds both web requests that could potentially cause an error.
Callbacks, Promises or Async/Await?
All the three methods to handle asynchronous code are possible. Which way to go depends on multiple factors:
- Callbacks work everywhere.
- Promises require at least ES 2015 / 6+.
- Async/await works in ES 2017+ and Node.js 7.6.
- All current browser versions support Async / Wait. Check at caniuse.com. Node.js supports it since 7.6+.
- If you need to work in an older environment and are unable to upgrade, you’re out of luck.
- If your library uses the old callback API, it’s simple to wrap the API to support promises and async/await. The Mozilla Developer website shows the code – it’s really simple and makes your code cleaner compared to mixing callbacks with newer methods.
In my opinion, I’d always go with the async / await syntax if possible. It results in the shortest code and it’s easiest to do everything right.