Error Handling with Express-React

I was recently working on error handling between the API and frontend of a single page shopping list app I’ve been designing and would like to share some simple strategies I used in the process.

The app’s backend is built with Node.js/Express connected to MongoDB via Mongoose, an ORM (object relation mapping) package for MongoDB. The front end is built with React.js whose local state is managed by Redux. The API calls are made via actions managed by thunk.

The following outlines how I set up validation for the User model to be used when a new user is created. Mongoose ships with some basic validation and sanitation for fields. Required is the most useful. The second argument of the required property is a custom message (I chose ‘An email is required’).

Mongoose purposely only offers basic validation out of the box, because it’s quite easy to set up custom validations and there are plenty of third party packages to supplement. Below I use a package called validator to check that the email is properly constructed. A custom validation is created by calling the validate and passing the value to the validation. The return value should be a boolean. In the case below the validation calls validator.isEmail(value). If it doesn’t pass, the method throws an error message.

One note about Mongoose schemas — assigning the ‘unique’ property to a particular field doesn’t cause an error to be thrown if you try to create a new instance with an identical field. The ‘unique’ validation is used for indexing fields.

// User schema with validation fieldsconst UserSchema = new Schema({
email: {
type: String,
required: [true, 'An email is required'],
unique: true,
trim: true,
lowercase: true,
validate(value) {
if (!validator.isEmail(value)) {
throw new Error('Not a proper email format');
}
},
},
password: {
type: String,
required: [true, 'A password is required'],
},
name: {
type: String,
trim: true,
required: [true, 'A name is required'],
},

I set up some of the simpler validation on the client side — checking that data was not left blank and that email addresses were in an acceptable format. Once the data is validated and sent to the backend, where validation is set up in Mongoose schemas and is executed as middleware. As a challenge, I rechecked the validation that I set up in the client in the backend, in addition to the server side validations. I also wanted to see if I could have the API send back specific error messages to the client, depending on what went wrong.

The actual validation happens in the router’s handler. In my setup, it’s a three step process:

  1. Check that the submitted email doesn’t yet exist in the database, that it is unique.
  2. Check that the password and password confirmation are the same.

These first two validations occur in the actual route handler and if either fails, the router sends back a status 400 with an error object to the frontend.

As an aside, in the client I am using Axios to make the requests. Because the errors are sent back with a 400 status code, they can be accessed in the catch block using

3. Create and save the new user. The schema validations happen as pre middleware just prior to saving. The error object looks like this:

{
name: ValidatorError: A name is required
{
properties: {
validator: [Function (anonymous)],
message: 'A name is required',
type: 'required',
path: 'name',
value: ''
},
kind: 'required',
path: 'name',
value: '',
reason: undefined,
[Symbol(mongoose:validatorError)]: true
},

In order to combine multiple validation errors (if they should occur), I set up an array of my field names (userFields). I call save() on the new user within a try-catch block. If there are no problems, the new user is sent back to the client. If there are errors, the catch block iterates through the error object, checking if the fields appear using bracket notation. If they do, I push the message into the foundErrors array. Finally I join the messages before sending them to the client.

const userFields = ['name', 'email', 'password'];
const user = new User(req.body);
try {
await user.save();
const token = await user.generateAuthToken();
res.status(200).send({ user, token });
} catch (e) {
const foundErrors = [];
userFields.forEach((field) => {
if (e.errors[field]) {
foundErrors.push(e.errors[field].message);
}
});
res.status(400).send({ error: foundErrors.join('. ') });
}

I’m sure there are other approaches, but this setup worked well for me and, as an added bonus, working out the code gave me some insight into Mongoose validation and error handling. This solution works nicely, and sends very specific information that can be directly used for notifications in the client.

Written by

After years of teaching music in Austin, Shanghai and Yangon, I’m making a career change to my other passion, software development.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store