Rozszerzanie błędu w Javascript ze składnią ES6 i Babel

133

Próbuję rozszerzyć Error o ES6 i Babel. To nie działa.

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

Obiekt Error nigdy nie otrzymuje odpowiedniego zestawu komunikatów.

Spróbuj w Babel REPL .

Teraz widziałem kilka rozwiązań na SO ( na przykład tutaj ), ale wszystkie wydają się bardzo nie-ES6-y. Jak to zrobić fajnie, ES6? (To działa w Babel)

Karel Bílek
źródło
2
Podążanie za linkiem do Babel REPL wydaje się wskazywać, że teraz działa poprawnie. Przypuszczam, że był to błąd w Babel, który został już naprawiony.
kybernetikos

Odpowiedzi:

191

Opierając się na odpowiedzi Karela Bíleka, wprowadziłbym małą zmianę w 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);

Spowoduje to wydrukowanie MyErrorna stosie, a nie na ogólnym Error.

Doda również komunikat o błędzie do śladu stosu - czego brakowało w przykładzie Karela.

Będzie również używać, captureStackTracejeśli jest dostępny.

W przypadku Babel 6, aby to zadziałało , potrzebujesz wbudowanego przekształcania ( npm ).

Lee Benson
źródło
1
@MichaelYounkin if (typeof Error.captureStackTrace === 'function') { Error.captureStackTrace(this, this.constructor.name) } else { this.stack = (new Error(message)).stack; } . Twierdziłbym, że lepiej jest użyć tej funkcji, jeśli jest dostępna, ponieważ zapewnia ona bardziej „natywny” stos wywołań i wyświetla nazwę obiektu błędu. Oczywiście, jeśli używasz tego również wyłącznie po stronie serwera (Node), to również nie stanowi problemu.
Lee Benson,
4
@MichaelYounkin Nie sądzę, żeby to zasługiwało na negatywną opinię. OP mówił o rozszerzaniu błędów w ES6. Zgodnie z tą logiką w co najmniej jednej przeglądarce brakuje prawie całej wersji ES6. Moje rozwiązanie (z dodaną funkcją sprawdzania funkcji) zapewnia natywne pokrycie w najczęściej używanej przeglądarce, rezerwę w każdej drugiej i 100% pokrycie w Node.js. Zgadzam się, że jeśli konsekwentnie, jaki błąd nazwa klasy, this.stack = (new Error(message)).stackdostaniesz to ... ale w praktyce prawdopodobnie nie jest to wielka sprawa.
Lee Benson
6
To nie działa w Babel 6:new MyError('foo') instanceof MyError === false
Sukima
5
Ten kod wstępnie skompilowany z babel jako modułem NPM: extendable-error-class npmjs.com/package/extendable-error-class, co jest wygodne, aby uniknąć zależności od babel-plugin-transform-
builtin
4
this.message = message;jest zbędny zsuper(message);
mathieug
39

Łącząc tę odpowiedź , tę odpowiedź i ten kod , stworzyłem tę małą klasę „pomocniczą”, która wydaje się działać dobrze.

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);

Spróbuj w REPL

Karel Bílek
źródło
1
this.stack = (new Error(message)).stack;- w przeciwnym razie wiadomość zniknie ze stosu
Lee Benson
3
Podejrzewam, że to nie działa w razie potrzeby, ponieważ jeśli to zrobisz: console.log (myerror instanceof ExtendableError); nadal mówi fałsz ..
Mauno Vähä
4
ten sam problem, użycie instanceof CustomError nie działa, o co chodzi, jeśli nie możesz użyć instanceof.
grecki
Można go ulepszyć, dodając messagew konstruktorze stosu błędów, aby po wyrzuceniu wyświetlał właściwy komunikat na górze stosu:this.stack = (new Error(message)).stack;
Sebastien
1
myerror.nameteraz zwraca „Błąd”. Nie wiem, czy jest to związane z nowszych wersjach babel.See @ sukima na odpowiedź poniżej
Eric H.
27

Aby w końcu to odłożyć. W Babel 6 jest jednoznaczne, że twórcy nie obsługują rozciągający się od wybudowania. Chociaż ta sztuczka nie pomaga z rzeczy, jak Map, Setitp to zadziała Error. Jest to ważne, ponieważ jedną z podstawowych idei języka, który może zgłosić wyjątek, jest zezwolenie na niestandardowe błędy. Jest to podwójnie ważne, ponieważ obietnice stają się bardziej użyteczne, ponieważ mają na celu odrzucenie błędu .

Smutną prawdą jest to, że nadal musisz to robić w stary sposób w ES2015.

Przykład w Babel REPL

Niestandardowy wzorzec błędu

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

Z drugiej strony jest wtyczka dla Babel 6, która to umożliwia.

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

Aktualizacja: (stan na 2016-09-29) Po kilku testach okazuje się, że babel.io nie uwzględnia poprawnie wszystkich potwierdzeń (rozszerzając niestandardowy rozszerzony błąd). Ale w Ember.JS błąd rozszerzania działa zgodnie z oczekiwaniami: https://ember-twiddle.com/d88555a6f408174df0a4c8e0fd6b27ce

Sukima
źródło
tak, z połączoną wtyczką babel działa poprawnie z zaakceptowaną odpowiedzią. (Jednak plik wynikowy nie działa w węźle, ponieważ nie mają odzwierciedlać, najwyraźniej)
Karel Bilek
Jako ciekawostka, jeśli specyfikacja ES2016 mówi, że wbudowane funkcje są rozszerzalne, dlaczego vms takie jak v8 i Babel es5 transpilują tak przeciwko temu? Czy nie jest rozsądnym oczekiwaniem, że klasa może rozszerzyć klasę w taki sam sposób, w jaki łańcuch prototypów może pochodzić z innych prototypów? Skąd potrzeba takiej ceramiki ukrytej we wtyczce?
Sukima
Jest to szczególnie frustrujące, gdy większość przypadków użycia chce po prostu tworzyć proste obiekty o wspólnym zachowaniu. Niestandardowy błąd, którego można użyć Error.toString(). Konieczność wykonywania specjalnych kółek i zakrętów, aby to osiągnąć, oznacza, że ​​większość programistów będzie tego unikać i ucieka się do złych praktyk, takich jak rzucanie strunami zamiast błędów. Albo tworzyć własne mapy jak obiekty. Skąd potrzeba odstraszania takich metod OOP?
Sukima
Moim zdaniem nie są temu przeciwni, to tylko kwestia techniczna. Nie jestem jednak pewien! Możesz ich zapytać :) projekty są dość otwarte
Karel Bílek
Jak dotąd wszystkie odpowiedzi z podobnych pytań na ten temat znajdują się pod adresem „babel nie potwierdza tego”. Pomyślałem, że to koniec rozmowy. Mój problem polega na tym, że brak wsparcia sprawia, że ​​powszechny idiom OOP jest trudny i musiałem nawet walczyć ze współpracownikami, aby pokonać ich szablonowy cruft. Chciałbym tylko, żeby było tutaj czyste alternatywne obejście. Wydaje się, że dodanie wtyczki jest wtedy najlepszym wyborem.
Sukima
15

Edycja : istotne zmiany w skrypcie maszynowym 2.1

Rozszerzanie elementów wbudowanych, takich jak Error, Array i Map, może już nie działać.

Zaleca się ręczne dostosowanie prototypu natychmiast po każdym wywołaniu super (...).

Edycja oryginalnej odpowiedzi Lee Bensona trochę mi odpowiada. To również dodaje stacki dodatkowe metody ExtendableErrorklasy do instancji.

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
źródło
1
Trzeba zadzwonić Object.setPrototypeOfw MyErrorkonstruktorze również. stackoverflow.com/a/41102306/186334 github.com/Microsoft/TypeScript-wiki/blob/master/ ...
CallMeLaNN
10

Wraz z ostatnimi zmianami w babel 6 uważam, że wbudowane rozszerzenie transformacji już nie działa. Skończyło się na tym mieszanym podejściu:

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;

i

import MyError from './MyError';

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

W rezultacie wszystkie te testy przechodzą:

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
źródło
7

Cytowanie

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

Nie ma potrzeby this.stack = (new Error()).stack;sztuczek dzięki super()wezwaniu.

Chociaż powyższe kody nie mogą wyprowadzać śladu stosu, chyba że this.stack = (new Error()).stack;lub Error.captureStackTrace(this, this.constructor.name);są wywoływane w Babel . IMO, to może jeden problem tutaj.

W rzeczywistości ślad stosu można wyprowadzić w ramach tych fragmentów kodu Chrome consolei za Node.js v4.2.1pomocą tych fragmentów kodu.

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);

Wyjście 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)

Wyjście 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
źródło
4

Oprócz odpowiedzi @zangw możesz zdefiniować swoje błędy w ten sposób:

'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');

który wyrzuci poprawną nazwę, wiadomość i stos:

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
źródło
4
To nie działa: new MyError('foo') instanceof MyError === false.
Sukima
1
Tak jest Node.js v7.7.3.
Gunar Gessner
2

Próbuję rozszerzyć Error z ES6

Ta class MyError extends Error {…}składnia jest poprawna.

Zauważ, że transpilery nadal mają problemy z dziedziczeniem po obiektach wbudowanych. W Twoim przypadku,

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

wydaje się, że rozwiązuje problem.

Bergi
źródło
Prawdziwe! Ale przekaz i tak nie jest ustawiony - napiszę nowy przykład.
Karel Bílek
Przepisałem teraz przykład
Karel Bílek
Dla mnie nie działa
Karel Bílek
Najwyraźniej „super (m)” zwróci pusty obiekt. Więc Object. assign nie pomoże.
Karel Bílek
@ KarelBílek: Z jakiej przeglądarki korzystasz? Error.call()zwraca dla mnie nową instancję błędu.
Bergi
2

Biorąc pod uwagę to, zaakceptowana odpowiedź już nie działa, zawsze możesz użyć fabryki jako alternatywy ( 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
źródło
Zaakceptowana odpowiedź nadal działa dla mnie, jeśli masz niezbędne wtyczki babel. Dziękuję też za tę odpowiedź!
Karel Bílek
2

Wolę mocniejszą składnię niż opisana powyżej. Dodatkowe metody typu error pomogą ci stworzyć ładną console.loglub coś innego.

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;
    }
}

Aby przetestować ten kod, możesz uruchomić coś podobnego:

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

    console.warn(message);
}

CustomErrorMile widziane jest rozszerzenie typu. Możliwe jest dodanie określonej funkcjonalności do typu rozszerzonego lub zastąpienie istniejącego. Na przykład.

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
źródło
1

Jak wspomina @sukima, nie można rozszerzać natywnego JS. Nie można odpowiedzieć na pytanie PO.

Podobnie jak odpowiedź Melbourne2991 , korzystałem raczej z fabryki, ale postępowałem zgodnie z zaleceniami MDN dotyczącymi typów błędów klientów .

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;
}
Eric H.
źródło
1

To działa dla mnie:

/**
 * @class AuthorizationError
 * @extends {Error}
 */
export class AuthorizationError extends Error {
    message = 'UNAUTHORIZED';
    name = 'AuthorizationError';
}
Michael Liquori
źródło
0

Nie używam Babel, ale w zwykłym ES6 wydaje mi się, że działa dobrze:

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

Testowanie z 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 ...'

Jak widać, stos zawiera zarówno nazwę błędu, jak i komunikat. Nie jestem pewien, czy czegoś mi brakuje, ale wszystkie inne odpowiedzi wydają się zbytnio komplikować.

JHH
źródło
0

Poprawiłem trochę rozwiązanie @Lee Benson w ten sposób:

extableError.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

przykład błędu

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 

Następnie możesz grupować błędy, mając specyfikatory opcji, aby zdecydować, co zrobić inaczej w niektórych sytuacjach specyficznych dla aplikacji

new AuthorizationError ("The user must be a seller to be able to do a discount", AuthorizationError.errors.BAD_PROFILE_TYPE )
Zied Hamdi
źródło