Skip to content

Instantly share code, notes, and snippets.

@jim-clark
Last active March 31, 2023 16:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jim-clark/d8c68db4e3d104dc277c49ab77513e5d to your computer and use it in GitHub Desktop.
Save jim-clark/d8c68db4e3d104dc277c49ab77513e5d to your computer and use it in GitHub Desktop.

Displaying Error Detail in the MERN-Stack

Scenario

When errors happen in the controller, typically due to validation errors when CUD'ing MongoDB/Mongoose documents, you might want to display specific error information to the user instead of a generic error message.

Let's assume that the schema for a User model has two properties, username & email, that need to be unique. When a user signs up, if either property is not unique, we want to let them know which property, username or email, caused the error.

The demonstration code we're going to use was created by cloning mern-infrastructure and updating the userSchema as follows:

// models/user.js

const userSchema = new Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true
  }
}, {
  timestamps: true,
  toJSON: {
    transform: function(doc, ret) {
      delete ret.password;
      return ret;
    }
  }
});

As you can see, the previous name property has been updated to username which, along with email, has been declared to be unique (unique: true).

Sending the Specific Error From the Server

Because the error will originate on the server, if you want to be able to display specific error information to the user, the server is going to have to provide the error information when responding to the request.

The existing code in the create action in controllers/api/users.js is already coded to send back the error object in the catch block:

async function create(req, res) {
  try {
    const user = await User.create(req.body);
    const token = createJWT(user);
    res.json(token);
  } catch (err) {
    res.status(400).json(err);
  }
}

So, there's nothing to change on the server!

JavaScript Error Objects

JavaScript has an Error Constructor/Class used to create error objects.

Error objects have a message property that contains the details of the error and is our best source of specific information about what caused the error.

However, some libraries might not create JavaScript specific error objects and might send back a different type of object, or maybe just a string, etc.

Logging and examining the error will be necessary to know how you might code the display logic in React.

The send-request.js Module

Because the sendRequest() function in src/utilities/send-request.js sent the request to the server, it's where the front-end receives the response from the server.

Currently, when the response object's ok property is false, the code is throwing an error object with a generic "Bad Request" message:

export default async function sendRequest(url, method = 'GET', payload = null) {
  
  ...

  const res = await fetch(url, options);
  // if res.ok is false then something went wrong
  if (res.ok) return res.json();
  throw new Error('Bad Request');
}

👀 When an error is thrown like above, it is handled by the first try/catch statement up the call-stack.

Instead of creating our own generic error object, we can refactor the code to provide the error received from the server so that the React code can use it:

export default async function sendRequest(url, method = 'GET', payload = null) {
  
  ...

  const res = await fetch(url, options);
  // if res.ok is false then something went wrong
  if (res.ok) return res.json();
  // Obtain the error sent by the server
  const error = await res.json(); 
  // Throw the error so that a try/catch block in React can use it
  throw error;
}

Awesome, the error will now be caught in SignUpForm.jsx where it can be used to display a more detailed message to the user!

Using the Error in SignUpForm.jsx

The code in the handleSubmit() function currently displays a generic "Sign Up Failed - Try Again" message when an error is caught:

  // src/components/SignUpForm/SignUpForm.jsx

  handleSubmit = async (evt) => {
    evt.preventDefault();
    try {
      const {name, email, password} = this.state;
      const formData = {name, email, password};
      // The promise returned by the signUp service
      // method will resolve to the user object included
      // in the payload of the JSON Web Token (JWT)
      const user = await signUp(formData);
      this.props.setUser(user);
    } catch {
      // An error occurred
      // Probably due to a duplicate email
      this.setState({ error: 'Sign Up Failed - Try Again' });
    }
  };

Let's begin refactoring the code to access the error that's been thrown:

  // src/components/SignUpForm/SignUpForm.jsx

  handleSubmit = async (evt) => {

    ...

    // Update the catch to access the error object
    } catch (e) {
      // Log the error object's message property
      console.log(e);

      // An error occurred
      // Probably due to a duplicate email
      this.setState({ error: 'Sign Up Failed - Try Again' });
    }
  };

Because we have not modified the form's state to match that of the updated User model, we can check out the error message!

Cool!

Refactor the form and state in SignUpForm.jsx

We need to update the code to make state and the sign up form compatible with the changes we made to the User model:

// src/components/SignUpForm/SignUpForm.jsx

...

export default class SignUpForm extends Component {
  state = {
    // Update name to username
    username: '',
    email: '',
    password: '',
    confirm: '',
    error: ''
  };

  ...

  handleSubmit = async (evt) => {
    evt.preventDefault();
    try {
      // Update name to username
      const {username, email, password} = this.state;
      const formData = {username, email, password};

    ...

  };

  render() {
    ...
          <form autoComplete="off" onSubmit={this.handleSubmit}>
            {/* Update the label and the input */}
            <label>Username</label>
            <input type="text" name="username" value={this.state.username} onChange={this.handleChange} required />
    ...

    );
  }
}

Now we can sign up for the first time without error.

However, if we log out and try signing up with the same username and/or email, we'll get an error:

Putting the Error to Use

The error information sent back by a given library will vary. So before we can put the error to use, we need to examine its information so that we can code the display logic.

In this case, we have an object resulting from a validation error in Mongoose:

{
  code: 11000,
  index: 0,
  keyPattern: {
    username: 1
  },
  keyValue: {
    username: "user1"
  }
}

A Google search will reveal that code: 11000 is a duplicate key error.

In our example, we know that an error is going to happen due to a non-unique username or email.

We can easily check for either property name in the object by first converting the object into a string and then use the includes() method to check if the property name is in the string:

// src/components/SignUpForm/SignUpForm.jsx

...

    } catch (e) {
      const errorString = JSON.stringify(e);
      if (errorString.includes('username')) {
        this.setState({ error: `Sorry, the username "${this.state.username}" is already taken` });
      } else if (errorString.includes('email')) {
        this.setState({ error: `Sorry, the email "${this.state.email}" is already taken` });
      }
    }

...

Bingo!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment