Repository: arb/celebrate (js)

Issue:
Celebrate errors no longer extend `Error`
- `node` version - 10.x
- `celebrate` version - 10.0.1
- `joi` version (via `npm ls --depth=0 | grep joi`) - N/A

Celebrate 10.0.0 [changed](https://github.com/arb/celebrate/commit/2598055f4a7fd7452404856e65e71b4822246af3#diff-6d186b954a58d5bb740f73d84fe39073R34-R39) the structure and context of the error objects that Celebrate produces (ie. the `err` that Celebrate passes into `next(err)`).  

This change appears to have broken some 3rd-party error-logging middlewares (specifically [sentry](https://sentry.io)) that expect errors passed to `next()` to extend the built-in JS `Error` object.

Would it be possible for `format` to start returning an `Error` object with a sensible `message` property, instead of a plain object?

Pull request description:
Use CelebrateError object instead of POJO
Closes #138

We now use error objects that extend the base `Error` object. Removed
`format` in favor of calling `CelebrateError()` directly. This way we
are always using "real" Error objects. This can be handy for things
outside of this library that want all Error objects to extend the
built in one.

Patch:
diff --git a/README.md b/README.md
index 80db3b8..000da30 100644
--- a/README.md
+++ b/README.md
@@ -127,10 +127,7 @@ Returns a `function` with the error handler signature (`(err, req, res, next)`).
 
 If the error format does not suite your needs, you are encouraged to write your own and check `isCelebrate(err)` to format celebrate errors to your liking. 
 
-Errors origintating from `celebrate()` are objects with the following keys:
-- `joi` - The full [joi error object](https://github.com/hapijs/joi/blob/master/API.md#errors).
-- `meta` - On `object` with the following keys:
-  - `source` - A [`Segments`](#segments) indicating the step where the validation failed.
+Errors origintating from `celebrate()` are [`CelebrateError`](#celebrateerrorerr-segment-opts)) objects.
 
 ### `Joi`
 
@@ -151,29 +148,28 @@ An enum containing all the segments of `req` objects that celebrate *can* valiat
 }
 ```
 
-### `isCelebrate(err)`
-
-Returns `true` if the provided `err` object originated from the `celebrate` middleware, and `false` otherwise. Useful if you want to write your own error handler for celebrate errors.
-
-- `err` - an error object
-
-### `format(err, segment, [opts])`
-
-Formats the incomming values into the shape of celebrate [errors](#errors())
+### `CelebrateError(err, segment, [opts])`
+Creates a new `CelebrateError` similar to the ones `celebrate` generates.
 
 - `err` - a Joi validation error object
-- `source` - A [`Segment`](#segments) indicating the step where the validation failed.
+- `segment` - A [`Segment`](#segments) indicating the step where the validation failed.
 - `[opts]` - optional `object` with the following keys
-  - `celebrated` -  `bool` that, when `true`, adds `Symbol('celebrated'): true` to the result object. This indicates this error as originating from `celebrate`. You'd likely want to set this to `true` if you want the celebrate error handler to handle errors originating from the `format` function that you call in user-land code. Defaults to `false`. 
+  - `celebrated` - `bool` that, when `true`, adds `Symbol('celebrated'): true` to the result object. This indicates this error as originating from `celebrate`. You'd likely want to set this to `true` if you want the celebrate error handler to handle errors originating from the `format` function that you call in user-land code. Defaults to `false`. 
 <details>
   <summary>Sample usage</summary>
 
   ```js
     const result = Joi.validate(req.params.id, Joi.string().valid('foo'), { abortEarly: false });
-    const err = format(result.error, Segments.PARAMS);
+    const err = CelebrateError(result.error, Segments.PARAMS);
   ```
 </details>
 
+### `isCelebrate(err)`
+
+Returns `true` if the provided `err` object originated from the `celebrate` middleware, and `false` otherwise. Useful if you want to write your own error handler for celebrate errors.
+
+- `err` - an error object
+
 ## Validation Order
 
 celebrate validates request values in the following order:
diff --git a/lib/index.d.ts b/lib/index.d.ts
index 5a811ba..a6f518d 100644
--- a/lib/index.d.ts
+++ b/lib/index.d.ts
@@ -6,16 +6,16 @@ import {
     ValidationResult 
 } from '@hapi/joi';
 
-enum Segments {
-    PARAMS="params",
-    HEADERS="headers",
-    QUERY="query",
-    COOKIES="cookies",
-    SIGNEDCOOKIES="signedCookies",
-    BODY="body"
-}
 
 declare namespace Celebrate {
+    declare enum Segments {
+        PARAMS="params",
+        HEADERS="headers",
+        QUERY="query",
+        COOKIES="cookies",
+        SIGNEDCOOKIES="signedCookies",
+        BODY="body"
+    }
     /**
     * Creates a Celebrate middleware function.
     */
@@ -67,20 +67,11 @@ declare namespace Celebrate {
     function isCelebrate(err: object): boolean;
 
     /**
-     * Format a joi error into a standard object
-     */
-    function format(err: ValidationError, 
-        source: Segment,
-        opts?: { celebrated: boolean }): {
-        meta: {
-            source: Segment
-        },
-        joi: ValidationError,
-    };
-    /**
-     * The different segments of req celebrate can valiate against
+     * The standard error used by Celebrate
      */
-    export const Segments: Segments;
+    declare class CelebrateError {
+        constructor(error: ValidationError, segment: Segments, opts?: { celebrated: boolean }) {}
+    }
 }
 
 export = Celebrate;
\ No newline at end of file
diff --git a/lib/index.js b/lib/index.js
index 907ecb8..89f9316 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -16,11 +16,14 @@ const internals = {
   },
 };
 
-internals.format = (error, segment, opts) => ({
-  [internals.CELEBRATED]: opts.celebrated,
-  joi: error,
-  meta: { source: segment },
-});
+internals.CelebrateError = class extends Error {
+  constructor(joiError, segment, opts) {
+    super(joiError.message);
+    this.joi = joiError;
+    this.meta = { source: segment };
+    this[internals.CELEBRATED] = opts.celebrated;
+  }
+};
 
 internals.validateSource = (segment, spec) => ({
   config,
@@ -31,7 +34,11 @@ internals.validateSource = (segment, spec) => ({
       value,
       segment,
     });
-  }).catch((e) => reject(internals.format(e, segment, internals.DEFAULT_FORMAT_OPTIONS)));
+  }).catch((e) => reject(new internals.CelebrateError(
+    e,
+    segment,
+    internals.DEFAULT_FORMAT_OPTIONS,
+  )));
 });
 
 internals.maybeValidateBody = (segment, spec) => (opts) => {
@@ -151,7 +158,7 @@ exports.errors = () => (err, req, res, next) => {
   return res.status(400).send(result);
 };
 
-exports.format = (error, segment, opts = { celebrated: false }) => {
+exports.CelebrateError = (error, segment, opts = { celebrated: false }) => {
   Assert.ok(error.isJoi);
 
   let result = sourceSchema.validate(segment);
@@ -159,7 +166,7 @@ exports.format = (error, segment, opts = { celebrated: false }) => {
   result = optSchema.validate(opts);
   Assert.ifError(result.error);
 
-  return internals.format(error, segment, opts);
+  return new internals.CelebrateError(error, segment, opts);
 };
 
 exports.Joi = Joi;


Test patch:
diff --git a/test/__snapshots__/celebrate.test.js.snap b/test/__snapshots__/celebrate.test.js.snap
index 1fddb21..f8850e8 100644
--- a/test/__snapshots__/celebrate.test.js.snap
+++ b/test/__snapshots__/celebrate.test.js.snap
@@ -40,33 +40,3 @@ Object {
   },
 }
 `;
-
-exports[`format() [async] returns a formatted error object without options 1`] = `
-Object {
-  "joi": [ValidationError: "value" must be [foo]],
-  "meta": Object {
-    "source": "body",
-  },
-  Symbol(celebrated): false,
-}
-`;
-
-exports[`format() [sync] returns a formatted error object without options 1`] = `
-Object {
-  "joi": [ValidationError: "value" must be [foo]],
-  "meta": Object {
-    "source": "body",
-  },
-  Symbol(celebrated): false,
-}
-`;
-
-exports[`format() returns a formatted error object with options 1`] = `
-Object {
-  "joi": [ValidationError: "value" must be [foo]],
-  "meta": Object {
-    "source": "body",
-  },
-  Symbol(celebrated): true,
-}
-`;
diff --git a/test/celebrate.test.js b/test/celebrate.test.js
index be13e69..0aec245 100644
--- a/test/celebrate.test.js
+++ b/test/celebrate.test.js
@@ -6,11 +6,10 @@ const {
   Joi,
   errors,
   isCelebrate,
-  format,
+  CelebrateError,
   Segments,
 } = require('../lib');
 
-
 describe('celebrate()', () => {
   describe.each`
     schema
@@ -415,7 +414,7 @@ describe('isCelebrate()', () => {
   });
 });
 
-describe('format()', () => {
+describe('CelebrateError()', () => {
   const schema = Joi.string().valid('foo');
   // Need a real Joi error to use in a few places for these tests
   const result = schema.validate(null);
@@ -425,32 +424,50 @@ describe('format()', () => {
     ${undefined}
     ${Error()}
     ${{}}
-    `('format($value)', ({ value }) => {
+    `('CelebrateError($value)', ({ value }) => {
   it('throws an error', () => {
     expect.assertions(1);
-    expect(() => format(value)).toThrow();
+    expect(() => CelebrateError(value)).toThrow();
   });
 });
   it('throws an error if the source is not a valid string', () => {
     expect.assertions(1);
-    expect(() => format(result.error, 'foo')).toThrow();
+    expect(() => CelebrateError(result.error, 'foo')).toThrow();
   });
   it('throws an error if the option arguments is incorrect', () => {
     expect.assertions(1);
-    expect(() => format(result.error, 'body', false)).toThrow();
+    expect(() => CelebrateError(result.error, 'body', false)).toThrow();
   });
   it('returns a formatted error object with options', () => {
-    expect.assertions(1);
-    expect(format(result.error, 'body', { celebrated: true })).toMatchSnapshot();
+    expect.assertions(2);
+    const err = CelebrateError(result.error, 'body', { celebrated: true });
+    expect(err).toMatchObject({
+      joi: expect.any(Joi.ValidationError),
+      meta: { source: 'body' },
+      message: expect.any(String),
+    });
+    expect(isCelebrate(err)).toBe(true);
   });
-  it('[sync] returns a formatted error object without options', () => {
-    expect.assertions(1);
-    expect(format(result.error, 'body')).toMatchSnapshot();
+  it('[sync] returns a CelebrateError object without options', () => {
+    expect.assertions(2);
+    const err = CelebrateError(result.error, 'body');
+    expect(err).toMatchObject({
+      joi: expect.any(Joi.ValidationError),
+      meta: { source: 'body' },
+      message: expect.any(String),
+    });
+    expect(isCelebrate(err)).toBe(false);
   });
-  it('[async] returns a formatted error object without options', () => {
-    expect.assertions(1);
+  it('[async] returns a CelebrateError object without options', () => {
+    expect.assertions(2);
     return schema.validateAsync(null).catch((e) => {
-      expect(format(e, 'body')).toMatchSnapshot();
+      const err = CelebrateError(e, 'body');
+      expect(err).toMatchObject({
+        joi: expect.any(Joi.ValidationError),
+        meta: { source: 'body' },
+        message: expect.any(String),
+      });
+      expect(isCelebrate(err)).toBe(false);
     });
   });
 });