Mongoose: beautiful mongodb native errors (unique - code 11000)

Created on 7 Sep 2014  路  34Comments  路  Source: Automattic/mongoose

I really miss a mechanism that would create validator-like unique index error message. Setting the index of an attribute to unique: true will tell mongodb to return ugly error message like this:

{ [MongoError: insertDocument :: caused by :: 11000 E11000 duplicate key error index: mydb-api.users.$email_1  dup key: { : "[email protected]" }]
  name: 'MongoError',
  code: 11000,
  err: 'insertDocument :: caused by :: 11000 E11000 duplicate key error index: mydb-api.users.$email_1  dup key: { : "[email protected]" }' }

This message should be treated as validator error message thus we could easily display error messages on a REST service.

// koa example
index: function*() {
    try {
      var email = new User(body);
      this.body = yield email.save();
    } catch(e) {
      this.body = e; // nice json errors to display
    }
}

Most helpful comment

mongoose-unique-validator is a plugin which adds pre-save validation for unique fields within a Mongoose schema. https://github.com/blakehaswell/mongoose-unique-validator

All 34 comments

+1

By the way, for any unique indexed fields, I have to check whether existent fields exists before creating new doc. This is duplicate work. It would be great that it is auto handled.

Something like

    if (11000 === err.code || 11001 === err.code) {
      var MongooseError = require('mongoose/lib/error')
      var valError = new MongooseError.ValidationError(err)
      valError.errors["xxx"] = new MongooseError.ValidatorError('xxx', 'Duplicate found', err.err)
      err = valError
    }

could be triggered after anything is received from a db. So every function like create, update and save should trigger this automatically before return. Let's wait to see what owners/others have to say about this.

Good suggestion, thanks.

mongoose-unique-validator is a plugin which adds pre-save validation for unique fields within a Mongoose schema. https://github.com/blakehaswell/mongoose-unique-validator

@vkarpov15 as of now I have a wrapper over mongoose, adding this.

Mongoose-unique-validator is doing it wrong. Pre-save validation is not a real solution here, because only unique index guarantees uniqueness.

user A: run pre-save, is the value unique? yes it is.
user B: run pre-save, is the value unique? yes it is.
user B: insert value
user A: insert value (BUG!)

Yep mongoose-unique-validator is not at all guaranteed to be unique, at least with multiple processes. The "plugin" tag is a reference to OP's issue about beautifying MongoErrors, which would be a valid plugin use case.

any progress?

Nope, but I think you might be able to do this as a plugin...

@xpepermint I'm doing that by a wrapper around save. No idea what @vkarpov15 means about using a plugin here (?)

I think the way to implement this would be a small separate module that does something like this:

module.exports = function(schema) {
  schema.methods.trySave = function(callback) {
    this.save(function(error, doc) {
      // Handle error logic here and callback();
    });
  };
};

And then any schema that wants to expose this trySave function would use schema.plugin(require('mongoose-beautiful-errors')); or whatever

Yes, that's kind of what I'm doing.

Persist method, like this:
https://github.com/iliakan/javascript-nodejs/blob/master/modules/lib/mongoose.js#L39

Yeah something like that as a standalone plugin would probably be a great idea. I like the general idea but I'm hesitant to put this into the mongoose core because it's a big backwards-breaking change and IMO doesn't really move the needle in terms of the experience of using mongoose.

I made a plugin based on your recommandations.
https://www.npmjs.com/package/mongoose-beautiful-unique-validation

My code is a little bit more versatile, so I stick to it for now, but thanks anyway ;)

Or does it support custom messages?

It does indeed, you just need to set the message as the value of unique.

var userSchema = mongoose.Schema({
    name: {
        type: String,
        unique: 'Custom error message'
    }
});

userSchema.plugin(require('mongoose-beautiful-unique-validation'));

Thanks for the plugin :) I'll mark this issue as closed.

I think it's should be out of the box feature. mongoose-beautiful-unique-validation is very hacky solution. I should use trySave instead of save. So I can't use other mongoose methods like create, update, findOneAndUpdate to get same validation error.

Or maybe we can use mongoose hooks to handle duplicate error? Like this:

schema.post('error', function(err, next) { // it can be moved to plugin
    if (err.code === 11000) {
        return next(new ValidationError(...));
    }

    next(err);
});

Model.create({}).catch(function(err) {
    if (err instanceof ValidationError) { 
        ...
    }
});

Yeah trySave() would be the best way right now. Hooks won't work because a core assumption of how hooks work is "if .save() fails, don't fire post save() hooks".

trySave() is very ugly way. So I propose to add error hook where I can replace original error object

That would be tricky, because post('error'); would imply handling more errors than just save errors. I would imagine having a catch-all for document-level errors though (validation, etc.) would be pretty useful though - would that still work?

We also should be able to handle model save errors (after Model.create(), Model.update(), etc.). It should be uniformly

Where can I find a list of Mongodb error codes?
11000is a duplicate key error code, but where can I find a list of the others?

@vkarpov15 anything changed here?

I upgraded to Mongoose 4.5.0 and mongo 3.2.7. I tried implementing the following post save hook but it is not working. The err object is not an error object but still the doc object.

Person.post('save', function (err, doc, next) {
  if (err) {
    next(err);
  } 
  else {
    next();
  }
});

It looks like the post save is getting unit tested against here.

I am triggering an 11000 mongo error but violating a unique index.

@VtoCorleone you need to define the post hook on a schema, not a model. Also, can you show your schema please?

@vkarpov15 as I recall this issue was about better handling of unique errors, to put them in line with regular validation errors.

May I ask, was anything changed here?

After I learned about these changes I tried to figure out how to use this feature to handle 11000 errors. If I understood correctly I should implement post hooks for all write methods: save, update, create, findOneAndUpdate (if I use all of them of course). Is it really good solution?

Additionally I had a question about Model.create(). What should I do to handle 11000 error for this method? Will some hook be called? I figured out that save hook works for Model.create() too. That's why I also created https://github.com/Automattic/mongoose/issues/4233

@Jokero yeah that's the general idea, youd also need one for insertMany. IMO it's the solution that adds the least surface area to the api - if you use mongoose, you know how to use hooks, so here's a slight modification to hooks to let you process errors

@vkarpov15 Does mongodb native errors have beautiful handling now?

Nope but you can write a plugin to do so pretty easily: http://thecodebarbarian.com/mongoose-error-handling.html

I created one that I expect to grow to handle most common MongoErrors, this one is using the 4.5 middlewares.

https://github.com/jhenriquez/mongoose-mongodb-errors

I strongly recommend everyone to use the well-documented package by @matteodelabre at https://github.com/matteodelabre/mongoose-beautiful-unique-validation per my PR here https://github.com/matteodelabre/mongoose-beautiful-unique-validation/pull/40. It is well-tested too and also supports a custom message formatted which builds on top of Mongoose standard error messages, e.g. mongoose.Error.messages.general.unique has a set default value of "Path `{PATH}` ({VALUE}) is not unique." if not already defined.

Furthermore, you can also use my new package mongoose-validation-error-transform at https://github.com/niftylettuce/mongoose-validation-error-transform, which will automatically transform the validation error message to a humanized and readable format.

Here's how to get started with both of these awesome packages:

npm install --save mongoose-beautiful-unique-validation mongoose-validation-error-transform
const mongooseBeautifulUniqueValidation = require('mongoose-beautiful-unique-validation');
const mongooseValidationErrorTransform = require('mongoose-validation-error-transform');

mongoose.plugin(mongooseBeautifulUniqueValidation);
mongoose.plugin(mongooseValidationErrorTransform);

For example, it will take a message of "full_name" is required and rewrite it to "Full name is required" automatically. It is also excellent for error handling, as it gives you a joined validation error message by comma (e.g. multiple props have validation errors in your schema).

If you wish to customize how the messages are joined, you can provide your own transformation (e.g. you may want to return an err.message that is a <ul> HTML tag of all the errors instead of a plain String joined by a comma. To do so simply pass an options object and override the default transform function.

mongoose.plugin(mongooseValidationErrorTransform, {
  // change this however you like, this is the default:
  transform: function(messages) {
    return messages.join(', '); 
  }
});

To output a <ul> list of error messages, of course you'd only want a <ul> if there's more than one:

mongoose.plugin(mongooseValidationErrorTransform, {
  // output a <ul> with bootstrap alpha 4 css classes
  transform: function(messages) {
    if (messages.length === 1) return messages[0];
    return `<ul class="text-xs-left mb-0"><li>${messages.join('</li><li>')}</li></ul>`;
  }
});

Complete default options for mongooseValidationErrorTransform are:

{
  capitalize: true,
  humanize: true,
  transform: function(messages) {
    return messages.join(', ');
  }
}
Was this page helpful?
0 / 5 - 0 ratings