Better Way To Write Async Function in Node/Express/Next - Handle catch(err) Only Once.
Avoid Writing a Lot of Try Catch by Catching The ‘catch()’ Just Once.
Deploy your code in seconds, with no infrastructure to manage. Qoddi App Platform is free for developers! Create an account today.
How annoying it is to write a lot of try-catch for each async function in an express app? What if you never need to write a try catch block for all async functions and still be able to handle the errors?
Prerequisites
- You need to know how node, express, middleware, routes and controller functions work together.
- You need to have an understanding of the err and next handlers of express.
- Understanding basic git and SSH.
- Understanding of NextJS
Try Catch
When you write an async function that can throw an error, you need to have the try-catch-finally blocks to handle the errors. Let’s say, you write a database query and it fails - the error would come in the catch.
async function(req, res, next){
Try{
//something to try
}catch(err){
//when trying to do something in try fails
}
}
If you don’t write a try-catch block in an async function, express would not be able to handle the error. You can read more about express error handling here.
The Solution
We are going to write a function called catchAsyncFunction(asyncFunction) that will take an async function as a parameter. Then it will catch the catch(err) of the async function. Then pass the error to express using next. Now the error will be available to handle in any middleware after the async function finishes.
We will be able to write it like this:
catchAsyncFunction(async function(req, res, next){
Try{
//something to try
}catch(err){
//when trying to do something in try fails
}
})
The catchAsyncFunction
So, this catchAsyncFunction is gonna be a little function. It is just 3 lines of code you need to have.
function catchAsyncFunction(asyncFunction){
return (req, res, next) => {
asyncFunction(req, res, next).catch(next)
}
}
Understanding this is a little confusing. But if you check the express error doc, express error handling - You can pass the catch block of a promise with the next handler.
If you call an async function and then catch the error, you can pass the error to express middleware using next.
The catchAsyncFunction takes an asyncFunction as a parameter. Then returns a callback that calls the asyncFunction, catches the error, and passes to the next middleware to use.
Handling the Passed Error
Now it is easy to handle the error. You can use a middleware at the bottom of all the route middleware. The middleware must have a new handler called err.
When a middleware function takes (err, req, res, next) this four, express thinks of it as an error middleware and passes the error to it. If you don’t have an err in any of your functions after the route, it will halt and throw an unhandled raw error.
const express = require(‘express’)
const app = express();
//your route middleware here
//error catch middleware after all routes
app.use((err, req, res, next)=>{
console.log(err.stack)
})
If no error happens, this middleware will be automatically skipped by express.
The Implementation
Start project
Let’s start locally with a folder named catchAsyncFunction and run:
npm init -y
npm i express
npm i -D nodemon
To start a node project, install the express framework and install nodemon as devDependency to automate server start on file change.
Paste this code in index.js:
const express = require("express");
const app = express();
//create catchAsyncFunction
function catchAsyncFunction(asyncFunction) {
return (req, res, next) => {
asyncFunction(req, res, next).catch(next);
};
}
//define your route here
const homeController = catchAsyncFunction(async (req, res, next) => {
//const data = await
//res.json({ msg: "no err" });
throw new Error("Error happened");
//this code will automatically go to error catch middleware
//no need next() because it passes to the next middleware defined after the route
});
//your route middleware here
app.get("/", homeController);
//catch error here
app.use((err, req, res, next) => {
console.log(err.stack);
res.status(400).json({
message: err.message,
});
});
//start server
app.listen(5000, () => {
console.log("Server started @5000");
});
Replace package.json script section with this code:
"scripts": {
"dev": "nodemon index.js",
"start": "node index.js"
},
Source code:
In the end, we will connect this to a front-end.
Avoiding try catch in frontend - NextJS
Again annoyingly, for each async function that fetches or sends server data, you have to write try catch blocks in the front-end too.
On top of that, In NextJS we have getServerSideProps or getStaticProps that can’t be used in child components. Only to use in root page components. Sometimes, useEffect is the best choice. So, I am going to use useEffect here.
Sometimes, you have to write a lot of useEffect hooks and have to write fetch(), try and catch multiple times. To avoid this, you can write a function that can handle all fetch() requests from all components.
The Solution
Instead of writing each time the try catch block like this:
useEffect(()=>{
const bringData = async ()=>{
try{
const data = await fetch(URL,
{
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
}
catch(err){
//handle error
}
}
bringData();
})
you can create a function called apiRequest() that can handle all the try-catch of all components and requests types. Usually, each of your fetch requests will have api URL, data, http method type. So, let’s make sure the function can accept them.
const apiRequest = (url, method, formData )=>{
if(method === ‘GET’){
// handle get request
}else{
// handle other request
}
}
Since, except GET requests POST, PATCH, DELETE can have a body property, we have to create an if else section to handle these two types of situations.
It's just two blocks that will handle all your requests. Let’s add an async keyword to it and add try catch once for all.
const apiRequest = async (url, method, formData )=>{
try{
if(method === ‘GET’){
// handle get request
}else{
// handle other request
}
}catch(err){
return err
}
}
The try can return data. Catch can return err.
Now in this code, you can add your fetch() to handle all requests like this:
const apiRequest = async (url, method, formData )=>{
try{
if(method === ‘GET’){
// handle get request
const response = await fetch(url,
{
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
const data = await response.json();
return data
}else{
// handle other request
const response = await fetch(url,
{
method: 'GET',
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const data = await response.json();
return data
}
}catch(err){
return err
}
}
The only difference between get and other http requests is the use of a body key. We use JSON.stringify(formData) to make a data object to a JSON object.
Since we are returning data and returning error in the catch block, the useEffect hook in the component level can get the data without throwing an error.
So no need to have a catch block there.
If we send a request to our backend, we will have a code like this:
useEffect(() => {
const fetchData = async () => {
const data = await apiRequest("backend url", "GET");
console.log(data);
}
fetchData();
});
Implement The Front End and Test It By Connecting to Our Previous Backend on Qoddi
To create a next app, in your project folder create a new directory called ‘lessAsync’. Inside the directory open a terminal and run:
npx create-next-app ./ -y
Now, you will see all the files and folders.
Inside index.js, remove everything and paste this code:
import Head from "next/head";
import { useEffect, useState } from "react";
import { apiRequest } from "../utils/apiRequest";
export default function Home() {
const [errData, errDataSet] = useState("");
useEffect(() => {
const fetchData = async () => {
const data = await apiRequest("http://localhost:5000", "GET");
errDataSet(data.message);
};
fetchData();
}, []);
return (
<div>
<Head>
<title>Less Async</title>
</Head>
<h1> Test frontend error handling</h1>
<h2>{errData}</h2>
</div>
);
}
Outside the pages folder, create a utils folder and create a file called apiRequest.js and then paste the code in it:
export const apiRequest = async (url, method, formData) => {
try {
if (method === "GET") {
console.log("ji");
// handle get request
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
const data = await response.json();
return data;
} else {
// handle other request
const response = await fetch(url, {
method: method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const data = await response.json();
return data;
}
} catch (err) {
return err;
}
};
Start the app and test it
Qoddi is a web server deployment service that updates your codebase on git push. Qoddi builds your code and starts the server following your package.json directives.
On top of that, Qoddi supports nodeJS out of the box. No extra Procfile needed.
Let’s deploy and test it in qoddi.com:
- Configure your package.json for specific node and npm version,
- Add cors and process.env.PORT,
- Set your build script and start script,
- Add your code to a github repository,
- Connect the repo from flashdrive dashboard,
- That’s it. The server will be built and started.
You will get a live server link and an internal link to test. But first, we need to update our package.json.
Update Backend and Frontend package.json
In package.json we need to specify node and npm version. Open a terminal and check your version with node -v and npm -v.
Open both package.json files in your catchAsync and lessAsync folder and add an ‘engines’ property:
“engines”: {
“node”: “16.13.x”,
“npm”: “8.1.x”
}
Also, we need to add the cors package to the backend to accept requests from the frontend. Install cors with
npm i cors
In the index.js file, after the line const app = express() add these two lines to add cors:
const app = express();
const cors = require(‘cors’);
app.use(cors)
In app.listen method, instead of 5000 as port, let’s add a PORT variable:
const PORT = process.env.PORT || 5000
app.listen(PORT, ()=>{
console.log(`Server started @${PORT}`)
})
Similar to Heroku, Qoddi uses a port number from process.env
Since we already have our start and build script, we just need to update them. In the backend, add a build script with “npm install” init.
”scripts”: {
“build”: “”npm install”,
“dev”: “nodemon index.js”,
”start”: “node index.js”
}
In the frontend, update the start script with:
“start”: “npm install && next start”
So, let’s create two git repos and push our code to them:
Let’s deploy
Create an account on https://qoddi.com and log in to your dashboard. Now click on the new app menu. Let’s create the backend first and then create the front-end.
Dev Apps are Free (up to 3 apps) on Qoddi.com, no credit card is required.