Providing clearer error messages in Javascript
As developers, we come across errors every single day. We don’t want to see errors, and we definitely don’t want to introduce them - but sadly it’s an everyday part of the job. So why is it that so many errors lack proper context, and look something like this:
Uncaught Error: Cannot read property 'foo' in Bar at <anonymous>:1:7
Ugh. So now we have a vague idea that maybe we’re calling an undefined property on an object, but we’re relying on our developer to have provided sensible variable names, and using our own domain knowledge of the application to infer exactly where the error is coming from. It’s not ideal.
When we’re creating our own applications, a good developer is conscious of what information they return to the end user (which may be another developer) at all times. This is something done well by some of the best API providers out there, such as Stripe or Twilio. In the Stripe API documentation, you have a clear expectation of exactly what an error is going to look like, and have a good idea of how to fix it.
How to write better error messages
Firstly, consider exactly what happened, and where. Let’s say that you are trying to let a user reset their password, and want to check whether their token has expired yet. Your code might look like this:
if (isExpired(new Date(user.passwordTokenExpiry))) {
throw new Error("invalid token")
}
There is no context as to what the token is. A JWT? Password reset token? Maybe something else. The developer (and by extension, the user) consuming this error is none the wiser. Separately, the developer who needs to look at this later, won’t know where the error is coming from. It provides a poor developer experience.
So how to fix this? Well, custom error classes to the rescue! As seen in the mdn documentation you can extend the Error object provided by javascript to create your own ones with far more context. For example:
class PasswordTokenError extends Error {
constructor(message) {
super(message)
this.name = "PasswordTokenError"
this.message = message
}
toJSON() {
return {
error: {
name: this.name,
message: this.message,
},
}
}
}
When we call this in our code, we can now provide a great deal more context:
if (isExpired(new Date(user.passwordTokenExpiry))) {
throw new PasswordTokenError("Password reset token has expired")
}
So an error is thrown as before, but a developer can now check what type of error has been thrown, and take action accordingly. The error returns JSON like the following:
{
"error": {
"name": "PasswordTokenError",
"message": "Password reset token has expired"
}
}
Conclusion
Although conceptually very simple, this is a really easy way to clean up your errors and make debugging a lot easier for you and other developers. It provides context and a uniform way to catch errors relating to your application.
I’ve just introduced this to my code at work and am confident that it will make life a lot easier when it comes to debugging!
With thanks to Iain Collins for his excellent article on this topic.