nodejs – asynchronous explained
In this article i will talk about asynchronous nodejs functions. I use express as the routing middleware and pug as the template engine. The code below shows the setup for the nodejs snippets used later on in this post:
/*
* Setup:
* - use 'express' for routing
* - use 'fs' for reading file from filesystem
* - use 'pug' as the templating engine
*/
const express = require('express')
const fs = require('fs')
const app = express()
const port = 3000
app.use(express.json());
app.use(express.urlencoded({ extended: true }))
app.set('view engine', 'pug')
We have setup the nodejs app with the code above. Now we are going to add the routes that we want to publish in our example app, see the code below:
// render input form which posts to /read route
app.get('/', (req, res) => {
res.render('index')
}
)
// read the file
app.post('/read', async (req, res, next) => {
fs.readFile('./test.txt', (err, data) =>{
if (err) {
next(err)
}
next(data)
})
});
app.listen(port, () => console.log(`Listening on port ${port}`))
When we navigate to ‘http://localhost:3000’ the route ‘/’ is processed. This will render our index.pug file which has the following contents:
html
head
body
form(method='POST' action='/read')
div
p Your email address
input#name.form-control(type='text', placeholder='Email address' name='email')
p
button.btn.btn-primary(type='submit') Sign up
Pressing the ‘Sign up’ button will take us to the ‘http://localhost:3000/read’ route. As you can see this route will try to read the ‘./test.txt’ file, The fs.readFile is an asynchronous function. When the readFile operation is complete it will execute the callback that is provided as the second parameter.
When an error occurs fs.readFile will execute the callback with an error object (the data parameter will be ‘undefined’). The error object contains, as you expect, a description of the error:
Error: ENOENT: no such file or directory, open './does_not_exist.txt'
If you would like to have a complete stacktrace in your error object you should change the line ‘next(err)’ to next(new Error(err)’, the resulting error after this change:
Error: Error: ENOENT: no such file or directory, open './does_not_exist.txt' at ReadFileContext.fs.readFile [as callback] (/home/uname/node/app.js:21:18) at FSReqWrap.readFileAfterOpen [as oncomplete] (fs.js:420:13)
The code above makes use of the callback function of the readFile method on the fs object. An alternative way of accomplishing the same result is to use promises. Lets have a look at the code below:
app.post('/read', (req, res, next) => {
new Promise((resolve,reject) => {
fs.readFile('./test.txt', 'utf-8', (err, data) => {
if (err) {
// in the case of error, reject the promise, executing the catch code block
reject(err);
}
else {
// in the case of success, resolve the promise, exectuing the then code block
resolve(data);
}
});
})
.then((data) => {
res.send(data)
})
.catch((err) => {
res.send(err)
})
});
As you can see we wrap our fs.readFile in a Promise object. A promise will be rejected on error (the file could not be found for example) and will be resolved on success. When a promise is rejected the catch code block will be executed. If the promise is resolved the then code block will be executed.
The advantage of using a promise is that we can wait on the result if we want to. The code below will wait for the readFile function to complete and then continues the /read handle after the wait promise line
app.post('/read', async (req, res, next) => {
let fileContent = null
let promise = new Promise((resolve,reject) => {
fs.readFile('./test.txt', 'utf-8', (err, data) => {
if (err) {
// in the case of error, reject the promise, executing the catch code block
reject(err);
}
else {
// in the case of success, resolve the promise, exectuing the then code block
resolve(data);
}
});
})
.then((data) => {
fileContent = data
})
.catch((err) => {
res.send(err)
})
await promise
if (fileContent) {
// the catch of the promise did not send(..) any text
res.send(fileContent)
}
});