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
).
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 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.
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!
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!
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:
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!