Extension de l'erreur en Javascript avec la syntaxe ES6 et Babel

132

J'essaye d'étendre l'erreur avec ES6 et Babel. Ça ne marche pas.

class MyError extends Error {
  constructor(m) {
    super(m);
  }
}

var error = new Error("ll");
var myerror = new MyError("ll");
console.log(error.message) //shows up correctly
console.log(myerror.message) //shows empty string

L'objet Error n'obtient jamais le bon ensemble de messages.

Essayez dans Babel REPL .

Maintenant, j'ai vu quelques solutions sur SO ( par exemple ici ), mais elles semblent toutes très peu ES6-y. Comment le faire d'une manière agréable ES6? (Cela fonctionne à Babel)

Karel Bílek
la source
2
Suivre votre lien vers Babel REPL semble indiquer que cela fonctionne correctement maintenant. Je suppose que c'était un bogue dans Babel qui a depuis été corrigé.
kybernetikos

Réponses:

188

Sur la base de la réponse de Karel Bílek, j'apporterais un petit changement au constructor:

class ExtendableError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
    if (typeof Error.captureStackTrace === 'function') {
      Error.captureStackTrace(this, this.constructor);
    } else { 
      this.stack = (new Error(message)).stack; 
    }
  }
}    

// now I can extend

class MyError extends ExtendableError {}

var myerror = new MyError("ll");
console.log(myerror.message);
console.log(myerror instanceof Error);
console.log(myerror.name);
console.log(myerror.stack);

Cela s'imprimera MyErrordans la pile, et non dans le générique Error.

Il ajoutera également le message d'erreur à la trace de la pile - qui manquait dans l'exemple de Karel.

Il utilisera également captureStackTraces'il est disponible.

Avec Babel 6, vous avez besoin de transform-builtin-extend ( npm ) pour que cela fonctionne.

Lee Benson
la source
1
@MichaelYounkin if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor.name) } else { this.stack = (new Error(message)).stack; } . Je dirais qu'il est préférable d'utiliser cette fonction si elle est disponible, car elle fournit une pile d'appels plus `` native '' et imprime le nom de l'objet d'erreur. Bien sûr, si vous l'utilisez uniquement du côté serveur (Node), ce n'est pas non plus un problème.
Lee Benson
4
@MichaelYounkin Je ne pense pas que cela mérite un vote défavorable. L'OP a parlé d'étendre les erreurs dans ES6. Suivant cette logique, presque tout ES6 est absent d'au moins un navigateur. Ma solution (avec le contrôle func ajouté) fournit une couverture native dans le navigateur le plus utilisé, une solution de repli dans tous les autres et une couverture à 100% dans Node.js. Je suis d'accord que si vous quel nom de classe d'erreur this.stack = (new Error(message)).stackvous obtient systématiquement cela ... mais en pratique, ce n'est probablement pas un gros problème.
Lee Benson
6
Cela ne fonctionne pas dans Babel 6:new MyError('foo') instanceof MyError === false
Sukima
5
Ce code pré-compilé avec babel comme module NPM: extendable-error-class npmjs.com/package/extendable-error-class, ce qui est pratique pour éviter une dépendance sur babel-plugin-transform-builtin-extend
brillout
3
this.message = message;est redondant avecsuper(message);
mathieug
39

En combinant cette réponse , cette réponse et ce code , j'ai créé cette petite classe "helper", qui semble bien fonctionner.

class ExtendableError extends Error {
  constructor(message) {
    super();
    this.message = message; 
    this.stack = (new Error()).stack;
    this.name = this.constructor.name;
  }
}    

// now I can extend

class MyError extends ExtendableError {
  constructor(m) {   
    super(m);
  }
}

var myerror = new MyError("ll");
console.log(myerror.message);
console.log(myerror instanceof Error);
console.log(myerror.name);
console.log(myerror.stack);

Essayez en REPL

Karel Bílek
la source
1
this.stack = (new Error(message)).stack;- sinon le message est absent du stacktrace
Lee Benson
3
Je soupçonne que cela ne fonctionne pas comme nécessaire, car si vous le faites: console.log (myerror instanceof ExtendableError); il dit toujours faux ..
Mauno Vähä
4
même problème, l'utilisation de instanceof CustomError ne fonctionne pas, quel est l'intérêt d'étendre si vous ne pouvez pas utiliser instanceof.
gre
Il peut être amélioré en ajoutant le messagedans le constructeur de pile d'erreur, de sorte qu'il affiche le bon message en haut de la pile lorsqu'il est lancé:this.stack = (new Error(message)).stack;
Sebastien
1
myerror.namerenvoie maintenant "Erreur". Je ne sais pas si cela est lié aux versions ultérieures de babel.Voir la réponse de @ sukima ci
Eric H.
27

Pour enfin mettre cela au repos. Dans Babel 6 , il est explicite que les développeurs ne prennent pas en charge allant de construction. Bien que cette astuce ne aide avec des choses comme Map, Set, etc. , il travaille pour Error. Ceci est important car l'une des idées fondamentales d'un langage qui peut lever une exception est d'autoriser les erreurs personnalisées. Ceci est doublement important car les promesses deviennent plus utiles puisqu'elles sont conçues pour rejeter une erreur .

La triste vérité est que vous devez toujours effectuer cela à l'ancienne dans ES2015.

Exemple dans Babel REPL

Modèle d'erreur personnalisé

class MyError {
  constructor(message) {
    this.name = 'MyError';
    this.message = message;
    this.stack = new Error().stack; // Optional
  }
}
MyError.prototype = Object.create(Error.prototype);

D'un autre côté, il existe un plugin pour Babel 6 pour permettre cela.

https://www.npmjs.com/package/babel-plugin-transform-builtin-extend

Mise à jour: (à partir du 29/09/2016) Après quelques tests, il semble que babel.io ne tient pas correctement compte de toutes les assertions (à partir d'une erreur étendue personnalisée). Mais dans Ember.JS, l'extension Error fonctionne comme prévu: https://ember-twiddle.com/d88555a6f408174df0a4c8e0fd6b27ce

Sukima
la source
oui, avec le plugin babel lié, cela fonctionne correctement avec la réponse acceptée. (Cependant, le fichier résultant ne fonctionne pas dans Node, car il n'a pas Reflect, apparemment)
Karel Bílek
Par curiosité, si les spécifications ES2016 disent que les builtins sont extensibles, pourquoi les vms comme la v8 et l'es5 de Babel se transforment-ils si contre cela? N'est-il pas raisonnable de s'attendre à ce qu'une classe puisse étendre une classe de la même manière qu'une chaîne de prototypes peut provenir d'autres prototypes? Pourquoi le besoin d'une telle céramique cachée dans un plugin?
Sukima
C'est particulièrement frustrant lorsque la plupart des cas d'utilisation veulent simplement créer des objets simples qui partagent un comportement. Une erreur personnalisée qui peut utiliser Error.toString(). La nécessité de faire des cercles et des girations spéciaux pour accomplir cela signifie que la plupart des développeurs l'éviteront et auront recours à de mauvaises pratiques comme lancer des cordes au lieu d'erreurs. Ou créer leur propre carte comme des objets. Pourquoi la nécessité de dissuader de telles méthodes POO?
Sukima
À mon avis, ils ne sont pas contre, c'est juste un problème technique. Je ne suis pas sûr cependant! Vous pouvez leur demander :) les projets sont assez ouverts
Karel Bílek
Jusqu'à présent, toutes les réponses des questions simulaires sur le sujet sont laissées à "Babel ne supporte pas cela". J'ai pensé que c'était la fin de la conversation. Mon problème est que le manque de soutien rend difficile un idiome OOP commun et j'ai même dû me battre avec des co-wokers pour les faire passer au-dessus de la crotte standard. Je souhaite juste qu'il y ait une solution alternative propre. Il semble que l'ajout d'un plugin soit alors le meilleur choix.
Sukima
15

Edit : changements de rupture dans Typescript 2.1

L'extension des fonctions intégrées telles que Error, Array et Map peut ne plus fonctionner.

À titre de recommandation, vous pouvez ajuster manuellement le prototype immédiatement après tout appel super (...).

La modification de la réponse originale de Lee Benson fonctionne un peu pour moi. Cela ajoute également stackdes méthodes de ExtendableErrorclasse supplémentaires à l'instance.

class ExtendableError extends Error {
   constructor(message) {
       super(message);
       Object.setPrototypeOf(this, ExtendableError.prototype);
       this.name = this.constructor.name;
   }
   
   dump() {
       return { message: this.message, stack: this.stack }
   }
 }    

class MyError extends ExtendableError {
    constructor(message) {
        super(message);
        Object.setPrototypeOf(this, MyError.prototype);
    }
}

var myerror = new MyError("ll");
console.log(myerror.message);
console.log(myerror.dump());
console.log(myerror instanceof Error);
console.log(myerror.name);
console.log(myerror.stack);
Artur Aleksanyan
la source
1
Vous devez également appeler Object.setPrototypeOfle MyErrorconstructeur. stackoverflow.com/a/41102306/186334 github.com/Microsoft/TypeScript-wiki/blob/master/…
CallMeLaNN
10

Avec les dernières modifications apportées à babel 6, je trouve que l' extension intégrée de la transformation ne fonctionne plus. J'ai fini par utiliser cette approche mixte:

export default class MyError {
    constructor (message) {
        this.name = this.constructor.name;
        this.message = message;
        this.stack = (new Error(message)).stack;
    }
}

MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

et

import MyError from './MyError';

export default class MyChildError extends MyError {
    constructor (message) {
        super(message);
    }
}

En conséquence, tous ces tests réussissent:

const sut = new MyError('error message');
expect(sut.message).toBe('error message');
expect(sut).toBeInstanceOf(Error);
expect(sut).toBeInstanceOf(MyError);
expect(sut.name).toBe('MyError');
expect(typeof sut.stack).toBe('string');

const sut = new MyChildError('error message');
expect(sut.message).toBe('error message');
expect(sut).toBeInstanceOf(Error);
expect(sut).toBeInstanceOf(MyError);
expect(sut).toBeInstanceOf(MyChildError);
expect(sut.name).toBe('MyChildError');
expect(typeof sut.stack).toBe('string');
Diego Ferri
la source
6

Citer

class MyError extends Error {
  constructor(message) {
    super(message);
    this.message = message;
    this.name = 'MyError';
  }
}

Il n'y a pas besoin de this.stack = (new Error()).stack;truc grâce àsuper() appel.

Bien que les codes ci-dessus ne puissent pas générer la trace de la pile, sauf si this.stack = (new Error()).stack;ou Error.captureStackTrace(this, this.constructor.name);est appelé dans Babel . OMI, c'est peut-être un problème ici.

En fait, la trace de la pile peut être sortie sous Chrome consoleet Node.js v4.2.1avec ces extraits de code.

class MyError extends Error{
        constructor(msg) {
                super(msg);
                this.message = msg;
                this.name = 'MyError';
        }
};

var myerr = new MyError("test");
console.log(myerr.stack);
console.log(myerr);

Sortie de Chrome console.

MyError: test
    at MyError (<anonymous>:3:28)
    at <anonymous>:12:19
    at Object.InjectedScript._evaluateOn (<anonymous>:875:140)
    at Object.InjectedScript._evaluateAndWrap (<anonymous>:808:34)
    at Object.InjectedScript.evaluate (<anonymous>:664:21)

Sortie de Node.js

MyError: test
    at MyError (/home/bsadmin/test/test.js:5:8)
    at Object.<anonymous> (/home/bsadmin/test/test.js:11:13)
    at Module._compile (module.js:435:26)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:311:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:134:18)
    at node.js:961:3
zangw
la source
4

En plus de la réponse @zangw, vous pouvez définir vos erreurs comme ceci:

'use strict';

class UserError extends Error {
  constructor(msg) {
    super(msg);
    this.name = this.constructor.name;
  }
}

// define errors
class MyError extends UserError {}
class MyOtherError extends UserError {}

console.log(new MyError instanceof Error); // true

throw new MyError('My message');

qui lancera le nom, le message et la trace de pile corrects:

MyError: My message
    at UserError (/Users/honzicek/Projects/api/temp.js:5:10)
    at MyError (/Users/honzicek/Projects/api/temp.js:10:1)
    at Object.<anonymous> (/Users/honzicek/Projects/api/temp.js:14:7)
    at Module._compile (module.js:434:26)
    at Object.Module._extensions..js (module.js:452:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Function.Module.runMain (module.js:475:10)
    at startup (node.js:117:18)
    at node.js:951:3
Honza Stepanovsky
la source
4
Cela ne fonctionne pas: new MyError('foo') instanceof MyError === false.
Sukima
1
C'est vrai Node.js v7.7.3.
Gunar Gessner
2

J'essaye d'étendre l'erreur avec ES6

Cette class MyError extends Error {…} syntaxe est correcte.

Notez que les transpileurs ont encore des problèmes avec l'héritage des objets intégrés. Dans ton cas,

var err = super(m);
Object.assign(this, err);

semble résoudre le problème.

Bergi
la source
Vrai! Mais le message n'est pas défini de toute façon - j'écrirai un nouvel exemple.
Karel Bílek le
J'ai réécrit l'exemple maintenant
Karel Bílek
Le "super (m)" renverra un objet vide, apparemment. Donc Object.assign n'aide pas.
Karel Bílek le
@ KarelBílek: Quel navigateur utilisez-vous? Error.call()renvoie une nouvelle instance d'erreur pour moi.
Bergi
2

Compte tenu de cela, la réponse acceptée ne fonctionne plus, vous pouvez toujours utiliser une usine comme alternative ( repl ):

function ErrorFactory(name) {
   return class AppError extends Error {
    constructor(message) {
      super(message);
      this.name = name;
      this.message = message; 
      if (typeof Error.captureStackTrace === 'function') {
        Error.captureStackTrace(this, this.constructor);
      } else { 
        this.stack = (new Error(message)).stack; 
      }
    }
  }     
}

// now I can extend
const MyError = ErrorFactory("MyError");


var myerror = new MyError("ll");
console.log(myerror.message);
console.log(myerror instanceof Error);
console.log(myerror.name);
console.log(myerror.stack);

Melbourne2991
la source
La réponse acceptée fonctionne toujours pour moi, si vous avez les plugins babel nécessaires. Merci pour cette réponse aussi!
Karel Bílek
2

Je préfère une syntaxe plus forte que celle décrite ci-dessus. Des méthodes supplémentaires au type d'erreur vous aideront à créer joli console.logou autre chose.

export class CustomError extends Error {
    /**
     * @param {string} message
     * @param {number} [code = 0]
     */
    constructor(message, code = 0) {
        super();

        /**
         * @type {string}
         * @readonly
         */
        this.message = message;

        /**
         * @type {number}
         * @readonly
         */
        this.code = code;

        /**
         * @type {string}
         * @readonly
         */
        this.name = this.constructor.name;

        /**
         * @type {string}
         * @readonly
         */
        this.stack = CustomError.createStack(this);
    }

    /**
     * @return {string}
     */
    toString() {
        return this.getPrettyMessage();
    }

    /**
     * @return {string}
     */
    getPrettyMessage() {
        return `${this.message} Code: ${this.code}.`;
    }

    /**
     * @param {CustomError} error
     * @return {string}
     * @private
     */
    static createStack(error) {
        return typeof Error.captureStackTrace === 'function'
            ? Error.captureStackTrace(error, error.constructor)
            : (new Error()).stack;
    }
}

Pour tester ce code, vous pouvez exécuter quelque chose de similaire:

try {
    throw new CustomError('Custom error was thrown!');
} catch (e) {
    const message = e.getPrettyMessage();

    console.warn(message);
}

Les extensions de CustomErrortype sont les bienvenues. Il est possible d'ajouter des fonctionnalités spécifiques au type étendu ou de remplacer l'existant. Par exemple.

export class RequestError extends CustomError {
    /**
     * @param {string} message
     * @param {string} requestUrl
     * @param {number} [code = 0]
     */
    constructor(message, requestUrl, code = 0) {
        super(message, code);

        /**
         * @type {string}
         * @readonly
         */
        this.requestUrl = requestUrl;
    }

    /**
     * @return {string}
     */
    getPrettyMessage() {
        const base = super.getPrettyMessage();

        return `${base} Request URL: ${this.requestUrl}.`;
    }
}
B. Bohdan
la source
1

Comme le mentionne @sukima, vous ne pouvez pas étendre le JS natif. Il est impossible de répondre à la question du PO.

Semblable à la réponse de Melbourne2991 , j'ai plutôt utilisé une usine, mais j'ai suivi la recommandation de MDN pour les types d'erreur des clients .

function extendError(className){
  function CustomError(message){
    this.name = className;
    this.message = message;
    this.stack = new Error().stack; // Optional
  }
  CustomError.prototype = Object.create(Error.prototype);
  CustomError.prototype.constructor = CustomError;
  return CustomError;
}
Éric H.
la source
1

Cela fonctionne pour moi:

/**
 * @class AuthorizationError
 * @extends {Error}
 */
export class AuthorizationError extends Error {
    message = 'UNAUTHORIZED';
    name = 'AuthorizationError';
}
Michael Liquori
la source
0

Je n'utilise pas Babel, mais dans ES6 simple, ce qui suit semble fonctionner correctement pour moi:

class CustomError extends Error {
    constructor(...args) {
        super(...args);
        this.name = this.constructor.name;
    }
}

Test de REPL:

> const ce = new CustomError('foobar');
> ce.name
'CustomError'
> ce.message
'foobar'
> ce instanceof CustomError
true
> ce.stack
'CustomError: foobar\n    at CustomError (repl:3:1)\n ...'

Comme vous pouvez le voir, la pile contient à la fois le nom et le message d'erreur. Je ne sais pas si je manque quelque chose, mais toutes les autres réponses semblent trop compliquer les choses.

JHH
la source
0

J'ai amélioré un peu la solution de @Lee Benson de cette façon:

extendableError.js

class ExtendableError extends Error {
    constructor(message, errorCode) {
        super(message);
        this.name = this.constructor.name;
        this.errorCode = errorCode
        if (typeof Error.captureStackTrace === 'function') {
            Error.captureStackTrace(this, this.constructor);
        } else {
            this.stack = (new Error(message)).stack;
        }
    }


}

export default ExtendableError

un exemple d'erreur

import ExtendableError from './ExtendableError'

const AuthorizationErrors = {
    NOT_AUTHORIZED: 401,
    BAD_PROFILE_TYPE: 402,
    ROLE_NOT_ATTRIBUTED: 403
}

class AuthorizationError extends ExtendableError {
    static errors = AuthorizationErrors 
}

export default AuthorizationError 

Ensuite, vous êtes en mesure de regrouper les erreurs tout en ayant des spécificateurs d'options pour décider quoi faire différemment dans certaines situations spécifiques à votre application

new AuthorizationError ("The user must be a seller to be able to do a discount", AuthorizationError.errors.BAD_PROFILE_TYPE )
Zied Hamdi
la source