JavaScript: пользовательские исключения

В JavaScript есть обработка исключений. try-catch-finally, throw, всё как у взрослых. Большинство программистов, правда, ими не пользуется, но не в этом суть.

И вот мы пишем, допустим, библиотеку и хотим, чтобы её функции в случае чего бросали определённые исключения. Язык позволяет нам здесь бросать что угодно, например, строку: throw "Ой, ошибка, ошибка!";.

Но, это не наш путь. Есть предопределённые типы ошибок, унаследованные от Error и есть стандарт, который говорит нам об объекте с полями name и message.

MDN показывает нам, как сделать свой «класс» исключений, унаследованный от Error:

// Create a new object, that prototypally inherits from the Error constructor.
function MyError(message) {
    this.name = "MyError";
    this.message = message || "Default Message";
}
MyError.prototype = new Error();
MyError.prototype.constructor = MyError;

Мозиловцы правда здесь согрешили и использовали для создания прототипа рабочий конструктор Error. Сделаем по правильному:

function MyError(message) {
    this.name = "MyError"; // нельзя в прототип положить - не будет отображаться
    this.message = message;
}
if (Object.create) {
    MyError.prototype = Object.create(Error.prototype);
} else {
    var Fake = function () {};
    Fake.prototype = Error.prototype;
    MyError.prototype = new Fake();
}
MyError.prototype.constructor = MyError;

Firebug

Всё здорово, но одно неприятно в Firefox’е. Когда выбрасывается исключение, запись об этом появляется в Firebug’е. Если это Error или другое стандартное исключение, то в фаербаге выводится строчка на которой произошло событие. По ней можно кликнуть и попасть в код, который вызвал исключение.

В случае же с нашим самопальным исключением, в Firebug’е просто висит унылое MyError: Oh, fuck, this is ERROR!. Никакой связи с кодом.

Причём в вышеприведённом варианте от MDN на первый взгляд строка кода показывается. Но при ближайшем рассмотрении она ведёт совршенно хрен знает куда.

В других же браузерах всё отлично, включая даже Internet Explorer. Можно выкидывать, хоть объект не унаследованный от Error, хоть просто строку. В консоле будет нормально отображено в каком файле на какой строке произошло несчастье.

Всё потому, что кроме message и name в разных браузерах, как всегда, свои наборы дополнительных полей. В Firefox среди прочего — fileName и lineNumber (и ещё columnNumber даже затесался). При создании объектов стандартных исключений они заполняются автоматически, а при создании пользовательских не хотят.

В варианте от MDN же, в качестве прототипа используется new Error(), который заполняется этими полями и они становятся доступны нам через цепочку прототипов. Но ведут они, понятное дело, всё время на ту строчку, где мы делали MyError.prototype = new Error().

Нужно как-то заполнить эти поля самостоятельно. А то кто-нибудь возьмёт нашу библиотеку, попользуется и скажет: у них какие-то палёные исключения, фу. И поморщится.

Error.stack

В большинстве браузеров объект исключений содержит ещё поле stack. В него записывается (неожиданно!) стек вызовов на момент создания объекта исключения. Записывается опять-таки только для стандартных «классов», но не для наших.

Как обычно, каждый браузер пишет в это поле стек в том виде, в котором ему придёт в голову. Правда, некоторые ушлые перцы, даже backtrace с его помощью делают.

В Firefox мы наблюдаем примерно такое (одна строка с переносами):

f3@http://test.loc/exc/:47
f2@http://test.loc/exc/:41
f1@http://test.loc/exc/:37
f2@http://test.loc/exc/:43
f1@http://test.loc/exc/:37
@http://test.loc/exc/:53

Теперь мы можем получить стек в любой точке программы, просто создав объект исключения:

function MyError(message) {
    var e = new Error(), matches;
    this.stack = e.stack;
    this.message = message;
    if (Error.prototype.fileName !== undefined) { // Firefox
        if (e.stack) {
            matches = (/^.*\n.*@(.*):(.*)\n/).exec(e.stack + "\n");
            if (matches) {
                this.fileName = matches[1];
                this.lineNumber= matches[2];
            }
        }       
    }
}

Здесь мы разбили стек на строки и взяли вторую (первая — вызов нашего MyError()). По правильному, конечно, можно было бы учесть, что FF может сменить формат стека и пройтись по всем строкам подбирая нужную, но чёрт с ним.

Теперь Firebug отображается всё, как у людей.

Можно было бы теперь восстановить stack в нашем объекте исключения у всех браузеров. Разобрать e.stack для каждого браузера в своём формате, вычистить лишние от конца вызовы и обратно склеить. Но я не настолько задрот, поэтому просто this.stack = e.stack.

UserException.create()

Ну и немного кода на последок.

/**
 * @namespace UserException
 */
var UserException = (function () {
 
    var Base, 
        create,
        isFileName = (Error.prototype.fileName !== undefined),
        Fake = function () {};
 
    /**
     * Создать пользовательский "класс" исключения
     *
     * @name UserException.create
     * @param {String} name
     * @param {Function} [parent=UserException]
     * @param {String} [defmessage=""]
     * @return {Function}
     */
    create = function (name, parent, defmessage) {
        var Exception, 
            proto,
            regexp;
 
        parent = parent || Base;
        defmessage = defmessage || "";
 
        Exception = function Exception(message) {
            var e = new Error(),
                matches;
            this.stack = e.stack;
            this.name = name;            
            this.message = (message !== undefined) ? message : defmessage;
 
            if (isFileName) {
                if (!regexp) {
                    regexp = new RegExp("^.*\n.*@(.*):(.*)\n");                    
                }
                matches = regexp.exec(e.stack + "\n");
                if (matches) {
                    this.fileName = matches[1];
                    this.lineNumber = parseInt(matches[2], 10);
                }                
            }
        };
 
        if (Object.create) {
            proto = Object.create(parent.prototype);
        } else {
            Fake.prototype = parent.prototype;
            proto = new Fake();
        }
        proto.constructor = Exception;
        Exception.prototype = proto;
 
        return Exception;
    };
 
    Base = create("UserException", Error);
    Base.Base = Base;
    Base.create = create;
 
    return Base;
}());

UserException.create(name, parent, defmessage) создаёт новый класс исключений. Аргументы:

  • name {String}: название класса
  • parent {Function}: родительский класс. По умолчанию сам UserException, унаследованный от Error.
  • defmessage {String}: сообщение по умолчанию, если не передано message в конструктор.

Создаём для нашей библиотеки иерархию исключений:

MyLib.Exceptions = (function () {
 
    var create = UserException.create,
        prefix = "MyLib.Exceptions.",
        Base = create("Base");
 
    return {
        'Base': Base,
        'NotFound': create(prefix + "NotFound", Base, "Not Found!!"),
        'Forbidden': create(prefix + "Forbidden", Base, "Forbidden!! Forbidden!!")
    };
 
}());

Используем:

try {
    throw new MyLib.Exceptions.Forbidden();
} catch (e if e instanceof MyLib.Exceptions.Base) { // "if" Firefox only
    console.log(e);
}

MyLib.Exceptions.Forbidden и MyLib.Exceptions.NotFound наследуется от MyLib.Exceptions.Base, та от UserException, а та от Error.

P.S, у Хрома есть дополнительное API для работы со стеком: http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi.

4 комментария »

  • Спасибо, хорошая статья, жаль что надо писать в js больше 2-х строчек кода для определения исключения. Конечно с помощью UserException все видимо будет круто, но хотелось бы чтобы это было реализовано в самом языке

    adw0rd, 22.03.2013, 23:24

  • >жаль что надо писать в js больше 2-х строчек кода для определения исключения

    ну, как и для любого «класса»

    vasa_c, 23.03.2013, 11:12

  • > ну, как и для любого “класса”

    в javascript

    adw0rd, 23.03.2013, 13:28

  • cпасибо за статью.
    Зачем нужна строчка?
    Base.Base = Base;

    Олег, 11.04.2016, 20:52

Leave a comment