frontend
JavaScript Error Handling from Express.js to React
Because errors are bound to happen and we need to handle them correctly.
Introduction
I've used a full stack MERN (MySQL, Express, React and Node.js) project I made from scratch for a whole handful of blog posts now. As I slowly built it out and added more bells and whistles to it, I learned a slew of new things worth sharing to help other devs avoid obstacles that I ran into along the way.
Past topics included using Sequelize as the ORM for a MySQL database, using PassportJS and JWT for authentication and protected routes, using Nodemailer to provide password reset capability via email, etc. Honestly, working through building this application, just for the sake of learning more Node, more React, more core JavaScript, taught me so much.
If you want to see the rest of my posts pertaining to this repo and the code itself, I’ll link to them all at the bottom of this article.
And today’s post is no exception.
Today, I’m talking about how to properly handle errors on both sides of a JavaScript application: from throwing errors on the server side in Express, to catching and handling them on the client side with React and Axios.
Express error handling
Since the server side is where the majority of the errors occur from, starting with the Node.js / Express server code is where I’ll begin as well.
I must confess, for quite a while, in the course of building this app, I passed back HTTP 200 status (the OK status) codes from the Express server even when an error like a missing JWT token or user ID was not found in the database.
Improper Express error handling example
res.status(200).json("404 - no user exists in db to update");
I did this because I wanted to send not just a 401 (unauthorized) or 403 (forbidden) HTTP status code, but because I also wanted to send a written message of what exactly the error was on the server side, and for the longest time, I couldn’t find out how to do both with Express.
It was either send an error message through res.json("This is an error message the client can read")
with a 200 HTTP status, or send a 401 or 403 HTTP status with no further info on what actually went wrong with a res.status(401)
.
Then one day, finally, I found the solution I was looking for buried deep in the Express documentation, of which there is quite a lot. And it really wasn’t as tricky as I thought — it was similar to how I sent a message along with a 200 HTTP status when something went right, like so:
Express success status code message example
res.status(200).send({ message: "user created" });
I could do the same with an error status and message.
Express failure status code and message example
res.status(401).send("no user exists in db to update");
Just by switching from Express’s .json()
to .send()
method meant I could chain together an HTTP error code and informational message for the client to decipher and then handle appropriately on the browser side. 😄
It is worth noting that .send()
can handle objects, strings, or arrays in JavaScript. The success message example is in the shape of an object with the property of message
while the failure response is just a string because in the event of a successful response things like a message, a JSON Web Token or other data might be sent over to the client from the server. My errors, however, needed nothing more than an HTTP response and error message as a string. Just so you know for the future. 🙂
Below is an example of one of the server side routes in my application showing the full implementation of sending either a success or failure when a user tries to update their personal info while logged in. (I’ve chosen to include it as both the nicely formatted and highlighted image from VS Code and a copy-able code snippet).
updateUser.js
module.exports = (app) => {
app.put("/updateUser", (req, res, next) => {
passport.authenticate("jwt", { session: false }, (err, user, info) => {
if (err) {
console.error(err);
}
if (info !== undefined) {
console.error(info.message);
res.status(403).send(info.message);
} else {
User.findOne({
where: {
username: req.body.username,
},
}).then((userInfo) => {
if (userInfo != null) {
console.log("user found in db");
userInfo
.update({
first_name: req.body.first_name,
last_name: req.body.last_name,
email: req.body.email,
})
.then(() => {
console.log("user updated");
res.status(200).send({ auth: true, message: "user updated" });
});
} else {
console.error("no user exists in db to update");
res.status(401).send("no user exists in db to update");
}
});
}
})(req, res, next);
});
};
Let me quickly break down what’s happening in this file.
The first thing that happens when the route is hit is that PassportJS takes over and checks to see if the user’s JSON Web Token both exists and is valid, this happens in another file and is covered in a different blog post so I won’t go in depth about that now.
Once a user’s been authenticated, the route goes through the process of attempting to find that username
in the MySQL database, and if it is found, the user info is updated and a success message with HTTP status 200 and a message of "user updated"
is sent back.
If the username
is not found in the database the server logs that the error that the user doesn’t exist and sends back a 401 HTTP status and message that "no user exists in db to update"
.
Here’s exactly what gets logged on the server side console that you’d see looking through the logs.
Error Express throws in the server logs
The main piece of code to focus on in terms of the error handling is this block right here towards the bottom of the file.
That’s about all there is to successfully sending both bad status codes and messages from Express to the browser. Now let’s talk about how the browser handles those errors.
{
console.error("no user exists in db to update");
res.status(401).send("no user exists in db to update");
}
React & Axios error handling
For the client side of my full stack application I chose to use the extremely popular promise based HTTP library Axios instead of the browser’s native Fetch API, as I find it easier to work with. This example will be shown using Axios for all HTTP requests and error handling.
Just as I was slowly improving the error handling on the server side, I was also improving my error handling on the client side as my application progressed and improved.
Before I properly handled the errors the server threw, this was all I was doing to interpret errors.
Poorly handling errors thrown from the server to the client example
.catch(error => {
console.log(error.data);
this.setState({
loadingUser: false,
error: true,
});
});
I would attempt to catch an error, log its message out to the console and then set the React application’s state accordingly. Not terrible for a start, but also not great for a final solution — the second I would try to throw something outside of a 200 HTTP status with a message, the client would just blow up in the catch()
block.
And just like with the server side, I finally got tired of this half baked error handling I was doing and found some better Axios documentation around how the library wants you to handle errors, which I’d overlooked before.
Turns out the way to read and handle errors with Axios is actually error.response.data
instead of just error.data
in the catch()
function. It’s not well highlighted but the documentation notes:
catch(error => {
if(error.response) {
/* the request was made and the server responded
with a status code that falls out of the range of 2xx */
console.log(error.response.data)
}
}
And with that one revelation, suddenly the error catching on the client side suddenly became much easier and more descriptive.
The way Axios interprets successful responses from the server is almost exactly the same way it interprets error codes.
Axios success response handling
try {
const response = await axios.get("api/findUser", {
params: {
username,
},
headers: { Authorization: `JWT ${accessString}` },
});
console.log(response.data);
// this.setState and so on after response is received...
}
Axios failure response handling
catch (error) {
console.log(error.response.data);
this.setState({
loadingUser: false,
error: true,
});
}
Which turned out to be much easier for me to update all my catch()
blocks to accept this new error format than I expected it to be.
Once more, here is a large snippet from the file where a user updates their data from the client side and sends it to be saved into the database.
UpdateProfile.js
updateUser = async (e) => {
const accessString = localStorage.getItem("JWT");
if (accessString === null) {
this.setState({
loadingUser: false,
error: true,
});
}
const {
first_name, last_name, email, username
} = this.state;
e.preventDefault();
try {
const response = await axios.put(
"http://localhost:3003/updateUser",
{
first_name,
last_name,
email,
username,
},
{
headers: { Authorization: `JWT ${accessString}` },
},
);
// eslint-disable-next-line no-unused-vars
console.log(response.data);
this.setState({
updated: true,
error: false,
});
} catch (error) {
console.log(error.response.data);
this.setState({
loadingUser: false,
error: true,
});
}
};
Here’s what’s happening in this function call.
First, updateUser()
pulls the JWT token from local storage, where it was previously added.
If the token is found, Axios constructs the body
of the PUT request which contains all the user’s new information, and sets the header
to the JWT token the server requires for authentication.
Finally, the response comes back as either successfully updated or as a failure, in which case the error.response.data
is logged to the console to help the developer figure out what went wrong and the app’s state is set to error
to help the user understand what happened as well.
This is what a user would see if the dev tools console was open when the failing request was made.
Error React throws in the console logs
The client sees the 401 status code the user sent back as well as the message from the server saying no user was found in the database with the matching user name.
The code that makes that happen is this snippet in particular.
} catch (error) {
console.log(error.response.data);
this.setState({
loadingUser: false,
error: true,
});
}
This wraps up my explanation of how the client can handle and display errors thrown from the server. Once again, not too tough to grasp, but just putting it all together can sometimes prove the trickiest part of all.
Conclusion
Errors in code are like death and taxes, they’re bound to happen. Our job as developers is to make sure we know when they happen and handle them gracefully so the user can have a good experience with our applications.
Although this type of thing is common place and of paramount importance, sometimes the documentation around such error handling is buried deep or is slightly cryptic with the bare bones code snippets provided. Which is why I wanted to write today detailing exactly how I pass error codes and messages from Express all the way to the Axios library being used on the React front end.
Check back in a few weeks, I’ll be writing more about JavaScript, React, ES6 or something else related to web development.
Thanks for reading, I hope I’ve given you some good solutions of how to handle errors in your own full stack JavaScript applications in the future. Errors will occur — let’s handle them well.
References & Further Resources
Want to be notified first when I publish new content? Subscribe to my newsletter.