Search code examples
resterror-handlingmongoose

Error handling with Mongoose


I am an absolute NodeJS beginner and want to create a simple REST-Webservice with Express and Mongoose.

Whats the best practice to handle errors of Mongoose in one central place?

When anywhere an database error occurs I want to return a Http-500-Error-Page with an error message:

if(error) {
  res.writeHead(500, {'Content-Type': 'application/json'});
  res.write('{error: "' + error + '"}');
  res.end();
}

In the old tutorial http://blog-next-stage.learnboost.com/mongoose/ I read about an global error listener:

Mongoose.addListener('error',function(errObj,scope_of_error));

But this doesn't seem to work and I cannot find something in the official Mongoose documentation about this listener. Have I check for errors after every Mongo request?


Solution

  • Simpler and more up to date solution IMO:

    Connection errors

    Handle connection level errors when you instantiate your db:

    const mongooseConnection = mongoose.createConnection(databaseURL)
    
    mongooseConnection.on('error', err => {
      throw new Error('Mongo database connexion error')
    })
    

    Query level errors

    Query level errors may appear if you write fields with wrong types or use query options (populate, sort...) incorrectly. We can use a more readable/up to date try catch await over the old promise.callback.catch syntax (mongoose doc)

    try {
      await Band.findOne({ _id: badId }).exec();
    } catch (err) {
      throw new Error('Mongo database connexion error')
    }
    

    BONUS: advices and good practices

    => Use an error handler across all your app

    Error handling in the 2 example above are not very relevant because you loose all contextual informations (mongo error msg, stack trace, database name, userId...)

    You may handle errors in a centralized way so that you can pass contextual informations alongside the error message like so:

    try { ... } catch (err) {
      throw ApplicationError(
        'myMessage',
        {
          // Here we pass additional informations
          // this will help handling the error on the express api side
          httpCode: err instanceof mongoose.Error.ValidationError ? 422 : 500,
          // with this one you can display original error messsage/stack trace
          originalError: err, 
          // other contextual informations may be useful to display
          // but be carreful not to display sensitive informations
          // here (Eg DB connection string)
          databaseName,
          userId,
          userRole,
          methodName,
        }
      )
    }
    
    class ApplicationError extends Error {
      constructor(errMsg, additionalInfos) {
        // Here handle error as you want
      }
    }
    

    List of all error types thrown by mongoose

    => Use a request handler to handle all mongoose queries the same way

    A common pattern to avoid duplicating code is to create a function to handle the execution of mongoose promise for you:

    async function afterRequest(mongoosePromise, { sort, page, limit }) {
      try {
        if (sort) promise.sort(sort)
    
        // PAGINATION
        if (page) promise.skip(localConfig.page * limit || 25).limit(limit || 25)
        else if (limit) promise.limit(limit)
    
        return await promise.exec()
    
      } catch (err) { /** handle error like in the above code */ }
    }
    

    So we can now use it like:

    const mongoosePromise = Band.find()
    afterRequest(mongoosePromise, { page: 1, limit: 10 })
    
    

    This pattern has the advantage of:

    • handling all in the same place, so it's the safest place to add custom code for security handling or validation for example
    • easily exposing some options like pagination... to api or some services that you may not want to deal directly with mongoose requests

    The above is juste a very simplified example, but to go further, here are some features we could implement:

    • check if the user has the permission to populate
    • masking certain fields in filter or updated/created fields related to user permissions (a simple user may not been able to write his permissions for example)
    • force certain filters (companyAdmin may have access only to their company so we want to enforce filter { companyId: user.companyId }
    • ...