How to Write Asynchronous Code in NodeJS
JavaScript is a non-blocking, single-threaded programming language. It will not go from top to bottom, running your functions one line at a time like you would expect.
For example, here is some simple code to read a file:
const fs = require("fs");
console.log("starting");
fs.readFile("/path/to/helloworld.txt", "utf8", (err, data) => {
if (err) console.log(err);
console.log(data);
});
console.log("finishing");
You may expect the result to be
starting
<file contents>
finishing
But instead you get:
starting
finishing
<file contents>
This is because JavaScript does not stop. It will keep going down your code while waiting for a process to finish. There are three ways to handle this and I'll go over them from worst to best.
The Humble Callback
To use callbacks for this code, you would do the following:
console.log("starting");
fs.readFile("/path/to/helloworld.txt", "utf8", (err, data) => {
if (err) {
console.log(err);
return; //or throw(err) or something else to strop the function
} else {
console.log(data);
console.log("finishing");
}
});
**Note: be sure to add a return after an error message and to use if/else to make sure the function does not continue if there is a problem.
Promises
You have to keep nesting callback functions within callback functions which can lead to deeply-nested code that is hard to read, better known as callback hell.
Promises are wonderful additions to JavaScript to rescue JavaScript developers from callback hell.
You can read more about Promises in depth and how to create them on MDN, but here is an example of how to consume them. Most APIs will have some way to use their code as a promise whether it is NodeJS's util.promisify
or AWS's .promise()
method for most of their API. For this example, we'll use promisify:
const fs = require("fs");
const { promisify } = require("util");
const ReadFilePromise = promisify(fs.readFile);
console.log("starting");
ReadFilePromise("/path/to/helloworld.txt", "utf8")
.then((data) => console.log(data))
.catch((err) => console.log(err))
.finally(() => console.log("finishing"));
You add a .then()
for the data, a .catch()
for the error, and a .finally()
for anything you want to do after the data or error is returned.
Async/Await
Finally we'll come to my favorite way to write JavaScript code, async/await. The async
keyword is syntactic sugar that allows a function to return a Promise
. So for this example we can use the same ReadFilePromise
from the last example. We'll need to wrap this logic inside an async
function and call it:
const ReadFileAsync = async(path) => {
console.log("starting");
try {
let data = await ReadFilePromise(path)
console.log(data)
} catch (error) {
console.log(error)
}
console.log("finishing")
}
ReadFileAsync("/path/to/helloworld.txt", "utf8")
NOTE: adding async to a function using callbacks does not make it work asynchronously. The function will still be using callback, but JavaScript now thinks it will return a Promise.
You want to wrap your await
inside a try/catch
to allow for error handling. Speaking of error handling...
How To Do Error Handling
To make sure your function bubbles the error up to the code using your function, throw
it!
Let's my ReadFileAsync
a function that another function can use.
const ReadFileAsync = async (path) => {
console.log("starting");
try {
return await ReadFilePromise(path);
} catch (error) {
throw error;
}
};
async function main() {
try {
let data = await ReadFileAsync("/path/to/helloworld.txt", "utf8");
console.log(data);
} catch (error) {
console.log(error);
} finally {
console.log("finishing");
}
}
main()
Top comments (0)