Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom errors and bluebird's catch with ErrorClass leads to inadvertent behaviour

I am trying to implement a module for custom errors.

It should be possible to instantiate an individual error within the require-statement of the app using this module:

var MyCustomError = require('custom-error')('MyCustomError');

This is the module:

'use strict';

var _CACHE = {};

function initError(name) {
  function CustomError(message) {
    this.name = name;
    this.message = message;
  }
  CustomError.prototype = Object.create(Error.prototype);
  CustomError.prototype.constructor = CustomError;
  _CACHE[name] = CustomError;
}

function createCustomError(name) {
  if (!_CACHE[name]) {
    initError(name);
  }
  return _CACHE[name];
}

module.exports = createCustomError;

The require-one-liner above is working so far.

Now, in my service, I want to catch this error explicitly:

var MyCustomError = require('custom-error')('MyCustomError')
// ...
return fooService.bar()
    .catch(MyCustomError, function (error) {
      logger.warn(error);
      throw error;
    })

If I reject the promise of fooService.bar in my test by throwing a MyCustomError this is working great.

BUT, this only works because my test and the service are using the same instance of MyCustomError.

For instance, if I remove the the caching-mechanism in my custom-error-module, the catch won't get reached/executed anymore, because bluebird does not understand that the two Errors are of the same type:

function createCustomError(name) {
  //if (!_CACHE[name]) {
    initError(name);
  //}
  return _CACHE[name];
}

The specific code of bluebird's handling is located in the catch_filter.js, you can have a look right here.

Though the approach does work within my app, this will sooner lead to problems once multiple modules are using the custom-error-module and the sharing of the same instances is not given any longer.

How can I get this concept up and running by not comparing the instances, but the error type itself?

Cheers,
Christopher

like image 889
Christopher Will Avatar asked Nov 19 '15 21:11

Christopher Will


1 Answers

I finally came up with a slightly different approach. For like-minded people this is the result:

ErrorFactory

var
  vsprintf = require("sprintf-js").vsprintf;

function CustomErrorFactory(code, name, httpCode, message) {

  // Bluebird catcher
  this.predicate = function (it) {
    return it.code === code;
  };

  this.new = function (messageParameters, details) {
    return new CustomError(messageParameters, details);
  };

  this.throw = function (messageParameters, details) {
    throw new CustomError(messageParameters, details);
  }; 

  function CustomError(messageParameters, details) {
    this.code = code;
    this.name = name;
    this.message = vsprintf(message, messageParameters);
    this.httpCode = httpCode;
    this.details = details || {};

    // Important: Do not swallow the stacktrace that lead to here.
    // @See http://stackoverflow.com/questions/8802845/inheriting-from-the-error-object-where-is-the-message-property
    Error.captureStackTrace(this, CustomError);
  }

  // CustomError must be instance of the Error-Object
  CustomError.prototype = Object.create(Error.prototype);
  CustomError.prototype.constructor = CustomError;
}

module.exports = CustomErrorFactory;

Errors

var
  ErrorFactory = require("./ErrorFactory");

function initErrors() {
  return {
    Parameter: {
      Missing: new ErrorFactory('1x100', 'ParameterMissing', 400, 'Parameter "%s" missing'),
      Invalid: new ErrorFactory('1x200', 'ParameterInvalid', 400, 'Parameter "%s" invalid')
      //..
    },
    Access: {
      NotAccessible: new ErrorFactory('3x100', 'AccessNotAccessible', 403, 'Resource "%s" is not accessible for "%s"'),
      //..
    },
    // ...
    Request: {
      //..
    }
  };
}

module.exports = initErrors();

I create a separate module containing these classes.

Then, in my implementation, I can catch errors like this individually:

function foo(request, reply) {

  return bluebird
    .resolve(bar)
    .then(reply)

    .catch(Errors.Parameter.Missing.predicate, function () {
      return reply(boom.badRequest());
    })

    .catch(Errors.Entity.NotFound.predicate, function () {
      return reply({}).code(204);
    })

    .catch(Errors.Entity.IllegalState.predicate, function (error) {
      return reply(boom.badImplementation(error.message));
    })

    // any other error
    .catch(function (error) {
      return reply(boom.badImplementation(error.message));
    });
}

Throwing

Errors.Entity.IllegalState.throw(['foo', 'bar']);
// or
throw Errors.Entity.IllegalState.new(['foo', 'bar']);

Require

Errors = require('errors'); // all
EntityErors = require('errors').Entity; // one group
EntityNotFoundError = require('errors').Entity.NotFound; // one particular

The only thing what I still don't understand id why is need to use a predicate-function rather than just passing the error-object to the catch-clause. But I can life with that.

like image 149
Christopher Will Avatar answered Nov 03 '22 00:11

Christopher Will