An async version of the user-helpers module
If you haven't already set up your Node sample project, you can quickly do so by following these instructions.
In a previous activity, we created a module of functions for working with users. But they did not make use of asynchronous code, which means that your app could freeze while the functions are running.
In order to make sense of this activity, it helps to understand asynchronous code (and 'callback hell') in JavaScript. I have to warn you that this activity will force you to experience 'callback hell'. Thankfully, JavaScript introduced promises so that you can avoid callback hell, and we'll be exploring them soon. But in this activity you'll get a taste of 'callback hell'.
Add a file named user-helpers-async.js to the modules folder, and put this code in it (you don't have to understand all of this code, just note that it makes heavy use of callbacks):
const fs = require("fs");
const path = __dirname + "/../users.json";
console.log(path);
function saveUsers(users, callback){
const json = JSON.stringify(users, null, 2)
fs.writeFile(path, json, (err) => {
if(err){
callback(err);
}else{
callback(null);
}
});
}
function getAllUsers(callback){
fs.readFile(path, "utf-8", (err, jsonString) => {
const users = JSON.parse(jsonString);
callback(err, users);
});
}
function addUser(user, callback){
getAllUsers((err, users) => {
if(err){
callback(err)
}else{
users.push(user);
saveUsers(users, callback);
}
});
}
function getUserByEmail(email, callback){
getAllUsers((err, users) => {
if(err){
callback(err)
}else{
const user = users.find(u => u.email === email);
if(user){
callback(null, user);
}else{
callback(new Error("Unable to find user by email: ", email));
}
}
});
}
function login(email, password, callback){
const user = getUserByEmail(email, (err, user) => {
if(err){
callback(err);
}else{
if(user && user.email === email){
callback(null, user);
}else{
callback(new Error("Login failed"));
}
}
});
}
module.exports.saveUsers = saveUsers;
module.exports.getAllUsers = getAllUsers;
module.exports.addUser = addUser;
module.exports.getUserByEmail = getUserByEmail;
module.exports.login = login;
If you do study the code, you'll see that there are references to callbacks everywhere, and that it is much more difficult to understand than the synchronized version of the user-helpers module.
Now we'll add a sample program so that we can try out the asynchronous functions. Add a file named users-async-sample.js to the samples folder, and put this code in it:
// MAKE SURE YOU ARE IMPORTING THE ASYNC VERSION (user-helpers-async)
const {saveUsers, getAllUsers, addUser, getUserByEmail, login} = require("../modules/user-helpers-async");
// Test the saveUsers() function
const userData = [
{"firstName":"Bob", "lastName":"Smith", "email":"bob@smith.com", "password":"test123"}
]
saveUsers(userData, (err) => {
if(err){
console.log("There was an error saving users", err);
}else{
console.log("Successfully saved users")
}
});
// Test the getAllUsers() function
const users = getAllUsers((err, users) => {
console.log("All Users: ", users);
});
// Test the addUser() function
const newUser = {"firstName":"Betty", "lastName":"Jones", "email":"betty@jones.com", "password":"opensesame"};
addUser(newUser, (err) => {
if(err){
console.log("Error adding user", err);
}else{
console.log("user added successfully");
}
});
// Test the getUserByEmail() function
getUserByEmail("bob@smith.com", (err, user) => {
if(err){
console.log("Error getting user by email", err)
}else{
console.log("result of getUserByEmail():", user);
}
})
// Test the login() function
login("bob@smith.com","test123", (err, user) => {
if(err){
console.log("Error logging in:", err);
}else{
console.log("User logged in:", user);
}
});
This code is not quite as bad as the code that's inside the user-helpers-async module. The important thing to note here is that none fo the functions we test are returning a value (unlike the synchronized version of the user-helpers module). Instead, to get a result from an asynchronous function, you must pass in a callback that includes a parameter for the result. In NodeJS, callback functions also include an 'err' parameter that represents an error. If there is no error, then this parameter will be undefined. This is known as the 'error first callback' pattern in NodeJS, and I tried to follow it when I designed the functions in user-helpers-async.js.
Go ahead and run the sample program with this command:
node samples/users-async-sample.js
So now you can say that you've had a good dose of callback hell! As a JavaScript developer, it's good to know a little bit about it. But as mentioned above there are newer and better ways to deal with asynchronous code. We'll be exploring them very soon!