Mongoose vs MongoDB - Whose error is it anyways?
MongoDB and Mongoose are an awesome combination for Typescript applications, but who handles what can be a little unclear.
If you're not from a programming background, if you mainly work with relational databases, or maybe you just thought the picture of the mongoose was cute, we are about to embark on an adventure that is figuring out how error handling in mongoose works.
Object Relational Mapping (ORMs) are a crucial part of working with non-relational databases. As the name suggests they allow you to create a relationship between the data in your database and the language you're writing in so that you can convert from:
- Database --> Object in memory
- Object in memory --> Database
This is a total simplification, but it's the core of the usage. So why is this crucial to non-relational databases? Non-SQL databases like MongoDB do not have defined schemas which means that I can throw whatever objects in there that I want and I don't have to explain at all!
Sure there's some huge benefits to being able to have a database that is flexible:
- You don't have to sit around and negotiate what the schema should be
- You don't have to worry about normalization (as much)
- You avoid JOIN hell (Who doesn't love 100 lines of SQL to select something)
The glaring problem is:
How do you ever know what you're going to get back?
This is what ORMs solve, it pushes the structure of the database into your code instead of in the DBMS (database management system). Which means, instead of
- Creating a bunch of SQL schemas
- Trying to match your objects to those schemas
We just:
- Create objects that represent the SQL schema
Back to the topic - Errors?
Okay we're going to go from 0 to 100 here.
After you've gone and looked up all of the nice little tutorials on how to set up MongoDB, how to create all your objects, build a whole stinking program, you're going to say:
Wait! How do I handle errors?
And this is when you're going to walk through something like the following.
I'm going to just install the typescript types to show how this works
This is the structure of the exceptions that could be thrown by mongoose:
Trying this out
This structure totally makes sense. You realize that users probably want to know how to recover from duplicate key errors so you say:
Let's write an exception handler for validation errors!
So you do it! You create some objects, you set up the database, you write a method to insert then you wrap it in something like this:
You run your code, and you see that you never actually hit this exception.
What went wrong?
Now you're wondering if maybe your data is wrong or you didn't set the primary key properly.
It's neither of those, you're using the wrong error. 😄
When Mongoose errors out, it doesn't throw the normal Mongoose exceptions. Instead it throws MongoDB errors
This makes sense if you understand what is going on. The errors that mongoose throws are associated with mongoose itself. So ValidationError
isn't a MongoDB validation error, it's a Mongoose validation error before it even tries to insert into the database.
Let's actually handle the errors
You actually need to handle the mongo exceptions as well! Hooray! But the types don't come native with Mongoose so you need to run the following to get them:
Now that we have the mongo errors, we can do error handling on that. We really care about MongoError
and MongoServerError
, from this you can see that mongo has a number system for their errors:
export declare class MongoError extends Error {
code?: number | string;
topologyVersion?: TopologyVersion;
connectionGeneration?: number;
cause?: Error;
constructor(message: string | Error);
get name(): string;
get errmsg(): string;
hasErrorLabel(label: string): boolean;
addErrorLabel(label: string): void;
get errorLabels(): string[];
}
export declare class MongoServerError extends MongoError {
codeName?: string;
writeConcernError?: Document;
errInfo?: Document;
ok?: number;
[key: string]: any;
constructor(message: ErrorDescription);
get name(): string;
}
You can find the list of exceptions here, but the core one we want if we want to handle validation errors is 11000 which means "Duplicate Key".
Actually handling mongo exceptions
So if we want to handle validation exceptions, we have to actually check if:
- We have gotten back a
MongoError
- If we have a code of 11000
So let's write that up instead:
Now you can take this concept and expand it to handle both Mongoose and MongoDB exceptions.
Handling the error but knowing what actually went wrong
Now you're probably thinking you're done, this is where you are wrong. The next question you should be asking is:
Nathan, what if I want to tell the user what fields were problematic?
And this is where things get really wild. In the mongo typescript library, when it throws MongoServerError
, they haven't indicated:
- What type of error this actually is in types
- What fields were problematic
Technically for 1, they tell you but you have to go and search it up on their website instead of strictly typing the outputs. But for 2 they have solved the problem by defining an index signature like this:
[key: string]: any
What this does is allows us to dynamically add fields to the object without typescript throwing a fit. This is perfect if you are returning variable key-value pairs like a hash-table, but is more of a cheeky workaround for not typing all of you objects if used outside.
If you are trying to solve this, because there is no solution to handle this with the tool itself, you can add in a type that extends the MongoServerError
class ActualMongoValidationError extends MongoServerError {
keyPattern: {
[key: string]: number;
};
keyValue: {
[key: string]: string;
};
}
try {
// Do your stuff in here
}
catch (e: MongoError) {
const properlyTypedError = error as ActualMongoValidationError;
}
This way you can actually see what is causing the error.