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.

Mongoose vs MongoDB - Whose error is it anyways?
Look at how cute he is! But watch out, you'll probably regret touching him.

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!

Everyone should be really scared right now

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

  1. Creating a bunch of SQL schemas
  2. Trying to match your objects to those schemas

We just:

  1. 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

npm i -D @types/mongoose

Installing the types for Mongoose

This is the structure of the exceptions that could be thrown by mongoose:

namespace Error {
  // A bunch of other classes
  export class ValidationError extends MongooseError {
    name: 'ValidationError';

    errors: { [path: string]: ValidatorError | CastError };
    addError: (path: string, error: ValidatorError | CastError) => void;

    constructor(instance?: MongooseError);
  }
}

Mongoose Exceptions: /mongoose/types/error.d.ts

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:

import { Error } from "mongoose";

try {
  // Insert a value into the database where a PK already exists
} catch (err: Error.ValidationError) {
  // Do something with the validation error
}

Really basic example of how you might try to handle validation errors

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. 😄

Don't cry it's okay, everything will be okay

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:

npm i -D @types/mongodb

Installing the types for MongoDB

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:

  1. We have gotten back a MongoError
  2. If we have a code of 11000

So let's write that up instead:

try {
  /* Our intentionally duplicate key insertion */
} catch (err: Error) {
  if(err instanceof MongoServerError) {
    switch(err.code) {
      case 11000: {
        /* Handle the error case */ 
      }
    }
  }
}

Basic approach to handling mongo errors

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:

  1. What type of error this actually is in types
  2. 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.