Handling errors in Mongoose/Express to display in React
Tags: React, Node, Error Handling, Mongodb, Frontend

To err is human, to create error handling middleware is divine. This article will explore how to catch and handle errors in Express using Mongoose and then display the errors to the end user using React.

All code used in this article is in this repo.

This diagram shows the modules we’ll discuss in the article. This app is using the MERN stack. The server is running through an Express app, and is connected to a MongoDB database. The frontend is a React app. These are the files we’ll be using for error validation:

Steps described:

Set up error handling middleware. Handle different errors. Send errors to the browser. Display error messages on the frontend. Highlight fields with errors on the frontend.

As you can see, this article has a few steps. It assumes some familiarity with Express, MongoDB/Mongoose, authentication, React/React router, and React hooks.

// Skip to step #2 if you have basic error handling middleware and validation already set up.

1. Set up error handling middleware

Express comes with error handling middleware that applies at the end of the middleware stack. They also let you define your own error handling middleware. Unlike other middleware, it takes four arguments. The first argument always stores the Error object, which is a built-in Node class that stores information about the error.

> app.js

app.use((err, req, res, next) => { console.log('congrats you hit the error middleware');

console.log(err); }

Our error handling middleware is going to be long so I’m going to save the function in a separate file. The error handling middleware goes at the end of the application stack, so it can “catch” errors all across the app. I’ve mounted them in my main app.js file—this is a simplified version of that file:

> app.js

const express = require('express'); const cors = require('cors'); const authRouter = require('./routers/authRouter'); const errorController = require('./controllers/errorController');

const app = express(); app.use(cors()); app.use(express.json());

app.use(express.urlencoded({extended: true})); app.use('/auth/', authRouter); app.use(errorController); module.exports = app;

In the errorController file, we define the behavior that will happen when Express detects an error. Above, we just console logged a string, but when we are communicating with the frontend, we will need to actually send a response object.

HTTP status codes for errors fall in the 400s and 500s. (All status codes can be found here.) We will be handling errors at user registration, so most of our status codes will be 400 Bad Request: Invalid syntax . But not ALL of our errors will be validation errors—we should plan for some errors that we can’t predict. An error catch-all is 500 Internal Server error: Situation the server doesn't know how to handle . This will always be the last line of defense when presented with an error.

>errorController.js

module.exports = (err, req, res, next) => {

res.status(500).send('An unknown error occurred.'); }

We want to tailor our errors, so the end user understands why they’re seeing an error message. How do we do this? I’m going to use authentication validation for all of these examples, because that case has many different errors that need different handling.

The errors thrown are defined by the validation I applied to my user model here. In this user model, we have the following possible errors:

Email or username is already taken. Username contains characters that aren’t letters/numbers. Password isn’t long enough. Passwords don’t match (users must confirm their passwords). One or more fields are empty.

const mongoose = require('mongoose'); const bcrypt = require('bcrypt'); const validator = require('validator'); const Schema = mongoose.Schema; const userSchema = new Schema({ username: { type: String, required: [true, 'Enter a username.'], unique: [true, 'That username is taken.'], lowercase: true, validate: [validator.isAlphanumeric, 'Usernames may only have letters and numbers.'] }, email: { type: String, require: [true, 'Enter an email address.'], unique: [true, 'That email address is taken.'], lowercase: true, validate: [validator.isEmail, 'Enter a valid email address.'] }, password: { type: String, required: [true, 'Enter a password.'], minLength: [4, 'Password should be at least four characters'] }, passwordConfirm: { type: String, required: [true, 'Retype your password.'], validate: { validator: function(el) { return el === this.password; }, message: 'Passwords don\'t match.' } } }); //schema middleware to apply before saving

userSchema.pre('save', async function(next) {

this.password = await bcrypt.hash(this.password, 12);

this.passwordConfirm = undefined; next(); }); const User = mongoose.model('User', userSchema); module.exports = User;

Above, I outlined five errors that are possible at registration. For MongoDB, there is an internal system of error classification. MongoDB assigns different names and codes to errors that can be seen by console logging an error. By console logging the errors, you can see what the error stacktrace (the error metadata and where it occurs in your app) looks like.

The stacktrace of a duplicate key error from MongoDB

Above, I outlined five errors that could occur with my registration validation. By using MongoDB’s classification, I can flag when those specific errors happen. Of the five errors I mentioned, they all fall into two different error categories:

Duplicate key error for existing usernames/emails. By looking at the stacktrace, I know this error has a code of E11000. Validation error for empty or poorly formatted fields. By looking at the stacktrace, I see this error’s name is ValidationError.

2. Handle different errors

We know there are two different errors we’re handling, and we have their code/name. Because MongoDB stores the Error class in the first argument of the error handling middleware, we can access properties of the Error object from that argument.

Our error handling middleware can now distinguish between two error types, and will resort to the catch-all error if the error isn’t flagged.

>errorController.js

module.exports = (err, req, res, next) => { try { console.log('congrats you hit the error middleware');

if(err.name === 'ValidationError') return err = handleValidationError(err, res);

if(err.code && err.code == 11000) return err = handleDuplicateKeyError(err, res); } catch(err) {

res.status(500).send('An unknown error occurred.'); } }

We split out the error handling for duplicate key errors and validation errors into individual functions. The function for duplicate key errors can look like this:

>errorController.js

//handle email or username duplicates const handleDuplicateKeyError = (err, res) => { const field = Object.keys(err.keyValue); const code = 409; res.status(code).send(`An account with that ${field} already exists.`); }

What’s going on here:

We can extract the field the error is occurring on by applying vanilla JS to the error object, which has a keyValue property built-in. Using the list of HTTP codes, we can determine that the correct code for duplicate entries in 409 (a request that conflicts with existing data in the server). We attach the code and a message to the response object with status and send . Using send is needed for showing error messages to the frontend. Send can push JavaScript (strings, objects, arrays) that can be immediately used in React, as opposed to the json method.

We not only want to show an error message on the frontend, but also highlight the fields that need to be corrected. So, in addition to pushing the message, we will also send the field that is throwing the error (either the username or email field) in an array.

//handle email or username duplicates const handleDuplicateKeyError = (err, res) => { const field = Object.keys(err.keyValue); const code = 409; const error = `An account with that ${field} already exists.`; res.status(code).send({messages: error, fields: field}); }

That was fairly simple. But note that we didn’t do two things: 1. use the error messages that we added to our user model, or 2. create an ability to handle multiple errors at once. We’ll do both in the validation function:

//handle field formatting, empty fields, and mismatched passwords const handleValidationError = (err, res) => { let errors = Object.values(err.errors).map(el => el.message); let fields = Object.values(err.errors).map(el => el.path); let code = 400; if(errors.length > 1) { const formattedErrors = errors.join(' '); res.status(code).send({messages: formattedErrors, fields: fields}); } else { res.status(code).send({messages: errors, fields: fields}) } }

In this function, we built on everything we did above and added:

Using el.message to get the error message we added in our user model. Looping through the error object to store multiple errors, if there is more than one. Detecting if errors > 1, and joining all messages together with a space, so they become one string.

Why do we need to join messages with a space? Without it, they will be an array of messages. To display it on the frontend, the error message will need to be a single string. To test this, I made a request from an API client (I’m using Google’s Advanced RESTful client), and purposefully left in two errors: the email field is blank and the passwords don’t match.

In the response below, you can see that the “messages” value is now a string, while the “fields” value is still an array.

We’re now ready to send error messages to the browser, and our error controller looks like this:

//handle email or usename duplicates const handleDuplicateKeyError = (err, res) => {

const field = Object.keys(err.keyValue);

const code = 409;

const error = `An account with that ${field} already exists.`;

res.status(code).send({messages: error, fields: field}); } //handle field formatting, empty fields, and mismatched passwords const handleValidationError = (err, res) => {

let errors = Object.values(err.errors).map(el => el.message);

let fields = Object.values(err.errors).map(el => el.path);

let code = 400; if(errors.length > 1) {

const formattedErrors = errors.join('')res

.status(code)

.send({messages: formattedErrors, fields: fields}); } else { res

.status(code)

.send({messages: errors, fields: fields}

}

} //error controller function module.exports = (err, req, res, next) => { try { if(err.name === 'ValidationError') return err = handleValidationError(err, res);

if(err.code && err.code == 11000) return err = handleDuplicateKeyError(err, res);

} catch(err) {

res

.status(500)

.send('An unknown error occurred.');

}

}

3. Send errors to the browser

How can we get these errors from Express → React? We need to move from making the API calls from a program like Google’s REST client or Postman, and into the browser.

My registration form looks like this. It’s in React component called Register.js.

The registration form from Register.js

I’m going to make this form stateful with a custom hook called useForm.js. In this hook, I will track and store the form values, and handle submit events.

Where error handling will come in is after the submit event, in the POST request to the server.

> useForm.js

import { useState } from 'react';

import { useHistory } from 'react-router-dom';

import axios from 'axios'; export default function useForm({ initialValues }) { let history = useHistory();

const [values, setValues] = useState(initialValues || {});

const [error, setError] = useState(null); //track form values

const handleChange = event => {

const value = event.target.value;

const name = event.target.name;

setValues({

...values,

[name]: value

});

}; //submit form when enter key is pressed

const handleKeyDown = event => {

const enter = 13;

if(event.keyCode === enter) {

handleSubmit(event);

}

} //submit form when submit button is clicked

const handleSubmit = event => {

event.preventDefault();

submitData({ values });

}; //send data to database

const submitData = async (formValues) => {

const url = 'http://127.0.0.1:5000/';

const dataObject = formValues.values;

const { username, email, password, passwordConfirm } = dataObject; try {

await axios ({

method: 'POST',

url: `${url}auth/register`,

data: {

username: username,

email: email,

password: password,

passwordConfirm: passwordConfirm

},

headers: new Headers({ 'Content-Type': 'application/json' })

}).then(res => {

history.push('/home');

})

} catch(err) {

setError(err.response.data);

}

}; return { handleChange,

handleKeyDown,

values,

handleSubmit,

error

}

}

When the POST request throws an error, we capture the error in err.response.data and store it state above. Then we return it from the hook, so we can use it in our registration form.

4. Display error messages on the frontend.

The value we’re returning is an object with a fields array and a messages string.

The message we want to display is in the messages value.

To show it in the registration form, we can make an Error component that conditionally appears when there is an error. The initial state of the error is null , so the error will evaluate false until the state is updated with a message.

> Register.js

import React from 'react';

import FormInput from './../components/FormInput';

import CTA from './../components/CTA';

import Prompt from './../components/Prompt';

import ConfirmPasswordInput from './../components/ConfirmPasswordInput';

import useForm from './../hooks/useForm';

import Error from './../components/Error'; export default function Register() { const { values, handleChange, handleKeyDown, handleSubmit, error } = useForm({ initialValues: {

email: '',

username: '',

password: '',

passwordConfirm: ''

}

}); return(

<div className="page">

<div className="inlineForm">

<h3>Register</h3>

<div className="inlineForm__notif">

{error && <Error error={error.messages}/> }

</div> <FormInput type={"email"} placeholder={"Email"} name={"email"} value={values.email} handleChange={handleChange} handleKeyDown={handleKeyDown} /> ///rest of the form fields

So, if there is an error, the Error component will appear and it will take the error.messages as a prop. Error component itself is very simple:

Error.js

import React from 'react'; export default function Error(props) { return(

<div className="notifError">

<h5>{props.error}</h5>

</div>

)

}

When an error is thrown, the notification appears at the top of the form:

The notification will persist until another request is made, but it could easily be dismissed automatically with a timeout function.

Now, the last part of this is how to make the fields that are throwing an error have a red highlight, so the end user is drawn to fix them.

5. Highlight fields with errors on the frontend.

Our fields are stored in the error.fields key. How can we dynamically reflect the error state of the field array, which looks something like this, error.fields = ["username", "password"] , in our form?

This is the register form again.

>Register.js

export default function Register() { const { values, handleChange, handleKeyDown, handleSubmit, error } = useForm({ initialValues: {

email: '',

username: '',

password: '',

passwordConfirm: ''

}

}); let invalidFields;

error ? invalidFields = error.fields : invalidFields = []; return(

<div className="page">

<div className="inlineForm">

<h3>Register</h3>

<div className="inlineForm__notif">

{error && <Error error={error.messages}/> }

</div> <FormInput type={"email"} placeholder={"Email"} name={"email"} value={values.email} handleChange={handleChange} handleKeyDown={handleKeyDown} fail={invalidFields.includes("email")}/> ///rest of the form fields

Initialize a variable called invalidFields. If there is an error (the error !== null), invalidFields is equal to the array of misused fields. If there is no error, it is equal to an empty array. Create a prop in the form field components called fail that will determine if the field is invalid or not. The value inside that prop should also evaluate to true or false . If invalidFields.includes('email') is true, the field will be invalid.

In the field component, we have to change its style based on if fail is true or false. If it is false, the style will stay the same. If it is true, we will add an error class to it that will apply the red border.

Here is the code for my FormInput component

>FormInput.js

import React from 'react'; export default function FormInput(props) {

let fail = props.fail; return(

<> <input type={props.type} name="input"

className={`input ${fail ? "input--fail" : null} `}

placeholder={props.placeholder} name={props.name}

value={props.value} onChange={props.handleChange}

onKeyDown={props.handleKeyDown}

/>

</>

)

}

In SCSS, the .input — fail class adds a red border around the fields that have the class applied.

Submitting an empty form will give every field the highlight.

That’s all on displaying errors in React from Express/Mongoose. There is a lot more that could be done here. If you’d like to see all the code from this demo, the repo link is here. The entire form and the server code can be found there.

Further reading about error handling: