В 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.
Спасибо, хорошая статья, жаль что надо писать в 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