Пользовательские ошибки, расширение Error
Когда что-то разрабатываем, то нам часто необходимы собственные классы ошибок для разных вещей, которые могут пойти не так в наших задачах. Для ошибок при работе с сетью может понадобиться HttpError
, для операций с базой данных DbError
, для поиска — NotFoundError
и т.д.
Наши ошибки должны поддерживать базовые свойства, такие как message
, name
и, желательно, stack
. Но также они могут иметь свои собственные свойства. Например, объекты HttpError
могут иметь свойство statusCode
со значениями 404
, 403
или 500
.
JavaScript позволяет вызывать throw
с любыми аргументами, то есть технически наши классы ошибок не нуждаются в наследовании от Error
. Но если использовать наследование, то появляется возможность идентификации объектов ошибок посредством obj instanceof Error
. Так что лучше применять наследование.
По мере роста приложения, наши собственные ошибки образуют иерархию, например, HttpTimeoutError
может наследовать от HttpError
и так далее.
Расширение Error
В качестве примера рассмотрим функцию readUser(json)
, которая должна читать данные пользователя в формате JSON.
Пример того, как может выглядеть корректный json
:
let json = `{ "name": "John", "age": 30 }`;
Внутри будем использовать JSON.parse
. При получении некорректного json
он будет генерировать ошибку SyntaxError
. Но даже если json
синтаксически верен, то это не значит, что это будет корректный пользователь, верно? Могут быть пропущены необходимые данные. Например, могут отсутствовать свойства name
и age
, которые являются необходимыми для наших пользователей.
Наша функция readUser(json)
будет не только читать JSON-данные, но и проверять их («валидировать»). Если необходимые поля отсутствуют или данные в неверном формате, то это будет ошибкой. Но не синтаксической ошибкой SyntaxError
, потому что данные синтаксически корректны. Это будет другая ошибка.
Назовём её ошибкой валидации ValidationError
и создадим для неё класс. Ошибка этого вида должна содержать информацию о поле, которое является источником ошибки.
Наш класс ValidationError
должен наследовать от встроенного класса Error
.
Класс Error
встроенный, вот его примерный код, просто чтобы мы понимали, что расширяем:
// "Псевдокод" встроенного класса Error, определённого самим JavaScript class Error { constructor(message) { this.message = message; this.name = "Error"; // (разные имена для разных встроенных классов ошибок) this.stack = <стек вызовов>; // нестандартное свойство, но обычно поддерживается } }
Теперь давайте унаследуем от него ValidationError
и попробуем новый класс в действии:
*!* class ValidationError extends Error { */!* constructor(message) { super(message); // (1) this.name = "ValidationError"; // (2) } } function test() { throw new ValidationError("Упс!"); } try { test(); } catch(err) { alert(err.message); // Упс! alert(err.name); // ValidationError alert(err.stack); // список вложенных вызовов с номерами строк для каждого }
Обратите внимание: в строке (1)
вызываем родительский конструктор. JavaScript требует от нас вызова super
в дочернем конструкторе, так что это обязательно. Родительский конструктор устанавливает свойство message
.
Родительский конструктор также устанавливает свойство name
для "Error"
, поэтому в строке (2)
мы сбрасываем его на правильное значение.
Попробуем использовать его в readUser(json)
:
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } // Использование function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("Нет поля: age"); } if (!user.name) { throw new ValidationError("Нет поля: name"); } return user; } // Рабочий пример с try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { *!* alert("Некорректные данные: " + err.message); // Некорректные данные: Нет поля: name */!* } else if (err instanceof SyntaxError) { // (*) alert("JSON Ошибка Синтаксиса: " + err.message); } else { throw err; // неизвестная ошибка, пробросить исключение (**) } }
Блок try..catch
в коде выше обрабатывает и нашу ValidationError
, и встроенную SyntaxError
из JSON.parse
.
Обратите внимание, как мы используем instanceof
для проверки конкретного типа ошибки в строке (*)
.
Мы можем также проверить тип, используя err.name
:
// ... // вместо (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
Версия с instanceof
гораздо лучше, потому что в будущем мы собираемся расширить ValidationError
, сделав его подтипы, такие как PropertyRequiredError
. И проверка instanceof
продолжит работать для новых наследованных классов. Так что это на будущее.
Также важно, что если catch
встречает неизвестную ошибку, то он пробрасывает её в строке (**)
. Блокcatch
знает, только как обрабатывать ошибки валидации и синтаксические ошибки, а другие виды ошибок (из-за опечаток в коде и другие непонятные) он должен выпустить наружу.
Дальнейшее наследование
Класс ValidationError
является слишком общим. Много что может пойти не так. Свойство может отсутствовать или иметь неверный формат (например, строка как значение возраста age
). Поэтому для отсутствующих свойств сделаем более конкретный класс PropertyRequiredError
. Он будет нести дополнительную информацию о свойстве, которое отсутствует.
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } *!* class PropertyRequiredError extends ValidationError { constructor(property) { super("Нет свойства: " + property); this.name = "PropertyRequiredError"; this.property = property; } } */!* // Применение function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } return user; } // Рабочий пример с try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { *!* alert("Неверные данные: " + err.message); // Неверные данные: Нет свойства: name alert(err.name); // PropertyRequiredError alert(err.property); // name */!* } else if (err instanceof SyntaxError) { alert("Ошибка синтаксиса JSON: " + err.message); } else { throw err; // неизвестная ошибка, повторно выбросит исключение } }
Новый класс PropertyRequiredError
очень просто использовать: необходимо указать только имя свойства new PropertyRequiredError(property)
. Сообщение для пользователя message
генерируется конструктором.
Обратите внимание, что свойство this.name
в конструкторе PropertyRequiredError
снова присвоено вручную. Правда, немного утомительно — присваивать this.name = <class name>
в каждом классе пользовательской ошибки. Можно этого избежать, если сделать наш собственный «базовый» класс ошибки, который будет ставить this.name = this.constructor.name
. И затем наследовать все ошибки уже от него.
Давайте назовём его MyError
.
Вот упрощённый код с MyError
и другими пользовательскими классами ошибок:
class MyError extends Error { constructor(message) { super(message); *!* this.name = this.constructor.name; */!* } } class ValidationError extends MyError { } class PropertyRequiredError extends ValidationError { constructor(property) { super("Нет свойства: " + property); this.property = property; } } // name корректное alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
Теперь пользовательские ошибки стали намного короче, особенно ValidationError
,
так как мы избавились от строки "this.name = ..."
в конструкторе.
Обёртывание исключений
Назначение функции readUser
в приведённом выше коде — это «чтение данных пользователя». В процессе могут возникнуть различные виды ошибок. Сейчас у нас есть SyntaxError
и ValidationError
, но в будущем функция readUser
может расшириться и, возможно, генерировать другие виды ошибок.
Код, который вызывает readUser
, должен обрабатывать эти ошибки.
Сейчас в нём используются проверки if
в блоке catch
, которые проверяют класс и обрабатывают известные ошибки и пробрасывают дальше неизвестные. Но если функция readUser
генерирует несколько видов ошибок, то мы должны спросить себя: действительно ли мы хотим проверять все типы ошибок поодиночке во всех местах в коде, где вызывается readUser
?
Часто ответ «Нет»: внешний код хочет быть на один уровень выше всего этого. Он хочет иметь какую-то обобщённую ошибку чтения данных. Почему именно это произошло — часто не имеет значения (об этом говорится в сообщении об ошибке). Или даже лучше, если есть способ получить подробности об ошибке, но только если нам это нужно.
Итак, давайте создадим новый класс ReadError
для представления таких ошибок. Если ошибка возникает внутри readUser
, мы её перехватим и сгенерируем ReadError
. Мы также сохраним ссылку на исходную ошибку в свойстве cause
. Тогда внешний код должен будет только проверить наличие ReadError
.
Этот код определяет ошибку ReadError
и демонстрирует её использование в readUser
и try..catch
:
class ReadError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'ReadError'; } } class ValidationError extends Error { /*...*/ } class PropertyRequiredError extends ValidationError { /* ... */ } function validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } } function readUser(json) { let user; try { user = JSON.parse(json); } catch (err) { *!* if (err instanceof SyntaxError) { throw new ReadError("Синтаксическая ошибка", err); } else { throw err; } */!* } try { validateUser(user); } catch (err) { *!* if (err instanceof ValidationError) { throw new ReadError("Ошибка валидации", err); } else { throw err; } */!* } } try { readUser('{bad json}'); } catch (e) { if (e instanceof ReadError) { *!* alert(e); // Исходная ошибка: SyntaxError:Unexpected token b in JSON at position 1 alert("Исходная ошибка: " + e.cause); */!* } else { throw e; } }
В приведённом выше коде readUser
работает так, как описано — функция распознаёт синтаксические ошибки и ошибки валидации и выдаёт вместо них ошибки ReadError
(неизвестные ошибки, как обычно, пробрасываются).
Внешний код проверяет только instanceof ReadError
. Не нужно перечислять все возможные типы ошибок
Этот подход называется «обёртывание исключений», потому что мы берём «исключения низкого уровня» и «оборачиваем» их в ReadError
, который является более абстрактным и более удобным для использования в вызывающем коде. Такой подход широко используется в объектно-ориентированном программировании.
Итого
- Мы можем наследовать свои классы ошибок от
Error
и других встроенных классов ошибок, но нужно позаботиться о свойствеname
и не забыть вызватьsuper
. - Мы можем использовать
instanceof
для проверки типа ошибок. Это также работает с наследованием. Но иногда у нас объект ошибки, возникшей в сторонней библиотеке, и нет простого способа получить класс. Тогда для проверки типа ошибки можно использовать свойствоname
. - Обёртывание исключений является распространённой техникой: функция ловит низкоуровневые исключения и создаёт одно «высокоуровневое» исключение вместо разных низкоуровневых. Иногда низкоуровневые исключения становятся свойствами этого объекта, как
err.cause
в примерах выше, но это не обязательно.
Error
objects are thrown when runtime errors occur. The Error
object can also be used as a base object for user-defined exceptions. See below for standard built-in error types.
Description
Runtime errors result in new Error
objects being created and thrown.
Error
is a serializable object, so it can be cloned with structuredClone()
or copied between Workers using postMessage()
.
Error types
Besides the generic Error
constructor, there are other core error constructors in JavaScript. For client-side exceptions, see Exception handling statements.
EvalError
-
Creates an instance representing an error that occurs regarding the global function
eval()
. RangeError
-
Creates an instance representing an error that occurs when a numeric variable or parameter is outside its valid range.
ReferenceError
-
Creates an instance representing an error that occurs when de-referencing an invalid reference.
SyntaxError
-
Creates an instance representing a syntax error.
TypeError
-
Creates an instance representing an error that occurs when a variable or parameter is not of a valid type.
URIError
-
Creates an instance representing an error that occurs when
encodeURI()
ordecodeURI()
are passed invalid parameters. AggregateError
-
Creates an instance representing several errors wrapped in a single error when multiple errors need to be reported by an operation, for example by
Promise.any()
. InternalError
Non-standard
-
Creates an instance representing an error that occurs when an internal error in the JavaScript engine is thrown. E.g. «too much recursion».
Constructor
Error()
-
Creates a new
Error
object.
Static methods
Error.captureStackTrace()
Non-standard
-
A non-standard V8 function that creates the
stack
property on an Error instance. Error.stackTraceLimit
Non-standard
-
A non-standard V8 numerical property that limits how many stack frames to include in an error stacktrace.
Error.prepareStackTrace()
Non-standard
Optional-
A non-standard V8 function that, if provided by usercode, is called by the V8 JavaScript engine for thrown exceptions, allowing the user to provide custom formatting for stacktraces.
Instance properties
Error.prototype.message
-
Error message. For user-created
Error
objects, this is the string provided as the constructor’s first argument. Error.prototype.name
-
Error name. This is determined by the constructor function.
Error.prototype.cause
-
Error cause indicating the reason why the current error is thrown — usually another caught error. For user-created
Error
objects, this is the value provided as thecause
property of the constructor’s second argument. Error.prototype.fileName
Non-standard
-
A non-standard Mozilla property for the path to the file that raised this error.
Error.prototype.lineNumber
Non-standard
-
A non-standard Mozilla property for the line number in the file that raised this error.
Error.prototype.columnNumber
Non-standard
-
A non-standard Mozilla property for the column number in the line that raised this error.
Error.prototype.stack
Non-standard
-
A non-standard property for a stack trace.
Instance methods
Error.prototype.toString()
-
Returns a string representing the specified object. Overrides the
Object.prototype.toString()
method.
Examples
Throwing a generic error
Usually you create an Error
object with the intention of raising it using the throw
keyword.
You can handle the error using the try...catch
construct:
try {
throw new Error("Whoops!");
} catch (e) {
console.error(`${e.name}: ${e.message}`);
}
Handling a specific error type
You can choose to handle only specific error types by testing the error type with the error’s constructor
property or, if you’re writing for modern JavaScript engines, instanceof
keyword:
try {
foo.bar();
} catch (e) {
if (e instanceof EvalError) {
console.error(`${e.name}: ${e.message}`);
} else if (e instanceof RangeError) {
console.error(`${e.name}: ${e.message}`);
}
// etc.
else {
// If none of our cases matched leave the Error unhandled
throw e;
}
}
Differentiate between similar errors
Sometimes a block of code can fail for reasons that require different handling, but which throw very similar errors (i.e. with the same type and message).
If you don’t have control over the original errors that are thrown, one option is to catch them and throw new Error
objects that have more specific messages.
The original error should be passed to the new Error
in the constructor’s options
parameter as its cause
property. This ensures that the original error and stack trace are available to higher-level try/catch blocks.
The example below shows this for two methods that would otherwise fail with similar errors (doFailSomeWay()
and doFailAnotherWay()
):
function doWork() {
try {
doFailSomeWay();
} catch (err) {
throw new Error("Failed in some way", { cause: err });
}
try {
doFailAnotherWay();
} catch (err) {
throw new Error("Failed in another way", { cause: err });
}
}
try {
doWork();
} catch (err) {
switch (err.message) {
case "Failed in some way":
handleFailSomeWay(err.cause);
break;
case "Failed in another way":
handleFailAnotherWay(err.cause);
break;
}
}
Note: If you are making a library, you should prefer to use error cause to discriminate between different errors emitted — rather than asking your consumers to parse the error message. See the error cause page for an example.
Custom error types can also use the cause
property, provided the subclasses’ constructor passes the options
parameter when calling super()
. The Error()
base class constructor will read options.cause
and define the cause
property on the new error instance.
class MyError extends Error {
constructor(message, options) {
// Need to pass `options` as the second parameter to install the "cause" property.
super(message, options);
}
}
console.log(new MyError("test", { cause: new Error("cause") }).cause);
// Error: cause
Custom error types
You might want to define your own error types deriving from Error
to be able to throw new MyError()
and use instanceof MyError
to check the kind of error in the exception handler. This results in cleaner and more consistent error handling code.
See «What’s a good way to extend Error in JavaScript?» on StackOverflow for an in-depth discussion.
Note: Some browsers include the CustomError
constructor in the stack trace when using ES2015 classes.
class CustomError extends Error {
constructor(foo = "bar", ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, CustomError);
}
this.name = "CustomError";
// Custom debugging information
this.foo = foo;
this.date = new Date();
}
}
try {
throw new CustomError("baz", "bazMessage");
} catch (e) {
console.error(e.name); // CustomError
console.error(e.foo); // baz
console.error(e.message); // bazMessage
console.error(e.stack); // stacktrace
}
Specifications
Specification |
---|
ECMAScript Language Specification # sec-error-objects |
Browser compatibility
BCD tables only load in the browser
See also
- A polyfill of
Error
with modern behavior like supportcause
is available incore-js
throw
try...catch
- The V8 documentation for
Error.captureStackTrace()
,Error.stackTraceLimit
, andError.prepareStackTrace()
.
Ошибки есть в каждом коде. Мы перевели гайд разработчика Айо Исайя, в котором он рассказывает о системе ошибок и о том, как их устранять.
Раз вы читаете эту статью, вы, конечно, знакомы с концепцией ошибок в программировании. Это ошибки в коде, они же баги, которые приводят к сбою или неожиданному поведению программы. В отличие от некоторых языков, таких как Go и Rust, где вы вынуждены взаимодействовать с потенциальными ошибками на каждом этапе пути, в JavaScript и Node.js можно обойтись без согласованной стратегии обработки ошибок.
Однако именно такая стратегия делает жизнь проще. Цель статьи — познакомить вас с этими шаблонами для создания, доставки и обработки потенциальных ошибок. Шаблоны помогут обнаружить и обработать потенциальные ошибки в коде до развёртывания.
Что такое ошибки в Node.js
Ошибка в Node.js — это любой экземпляр объекта Error. Общие примеры включают встроенные классы ошибок: ReferenceError
, RangeError
, TypeError
, URIError
, EvalError
и SyntaxError
. Пользовательские ошибки также можно создать путём расширения базового объекта Error, встроенного класса ошибки или другой настраиваемой ошибки. При создании ошибок таким путём нужно передать строку сообщения, описывающую ошибку. К сообщению можно получить доступ через свойство message
объекта. Объект Error
также содержит свойства name
и stack
, которые указывают имя ошибки и точку в коде, в которой объект создаётся.
const userError = new TypeError("Something happened!");
console.log(userError.name); // TypeError
console.log(userError.message); // Something happened!
console.log(userError.stack);
/*TypeError: Something happened!
at Object.<anonymous> (/home/ayo/dev/demo/main.js:2:19)
<truncated for brevity>
at node:internal/main/run_main_module:17:47 */
Функции объекта Error можно передать или вернуть из функции. Если бросить его с помощью throw
, объект Error станет исключением. Когда вы передаёте ошибку из функции, она переходит вверх по стеку, пока исключение не будет поймано. В противном случае uncaught exception
может обвалить всю работу.
Как обработать ошибку
Оптимальный способ обработки ошибок функции JavaScript зависит от того, выполняет ли эта функция синхронную или асинхронную операцию. Рассмотрим четыре общих шаблона, позволяющих обрабатывать ошибки функций в Node.js.
Исключения
Чаще всего ошибки функций обрабатывают путём генерации. В этом случае ошибка становится исключением, после чего её можно поймать где-нибудь в стеке с помощью блока try / catch
. Если у ошибки есть разрешение всплывать в стеке, не будучи перехваченной, она преобразуется в формат uncaughtException
, что приводит к преждевременному завершению работы приложения. Например, встроенный метод JSON.parse ()
выдаёт ошибку, если строковый аргумент не является допустимым объектом JSON.
function parseJSON(data) {
return JSON.parse(data);
}
try {
const result = parseJSON('A string');
} catch (err) {
console.log(err.message); // Unexpected token A in JSON at position 0
}
Для использования этого шаблона в функциях нужно добавить ключевое слово throw
перед экземпляром ошибки. Этот шаблон сообщения об ошибках и обработки идиоматичен для функций, выполняющих синхронные операции.
function square(num) {
if (typeof num !== 'number') {
throw new TypeError(`Expected number but got: ${typeof num}`);
}
return num * num;
}
try {
square('8');
} catch (err) {
console.log(err.message); // Expected number but got: string
}
Колбэк с первым аргументом-ошибкой
Из-за своей асинхронной природы Node.js интенсивно использует функции колбэка для обработки большей части ошибок. Колбэк (обратный вызов) передаётся в качестве аргумента другой функции и выполняется, когда последняя завершает свою работу.
Node.js использует колбэк с первым аргументом-ошибкой в большинстве асинхронных методов, чтобы гарантировать проверку ошибок до результатов операции. Колбэк обычно является последним аргументом функции, инициирующей асинхронную операцию, и вызывается один раз при возникновении ошибки или получении результата:
function (err, result) {}
Первый аргумент зарезервирован для объекта ошибки. Если ошибка возникает в ходе асинхронной операции, она доступна через аргумент err
при неопределённом результате. Однако, если ошибки не возникает, err
будет иметь значение null
или undefined
, а result
будет содержать ожидаемый результат операции. Этот шаблон работает, если прочитать содержимое файла с помощью встроенного метода fs.readFile ():
const fs = require('fs');
fs.readFile('/path/to/file.txt', (err, result) => {
if (err) {
console.error(err);
return;
}
// Log the file contents if no error
console.log(result);
});
Метод readFile ()
использует колбэк в качестве своего последнего аргумента, который, в свою очередь, соответствует подписи функции «первая ошибка». В этом сценарии result
включает в себя содержимое файла, который читается, если ошибки не возникает. В противном случае он определяется как undefined
, а аргумент err
заполняется объектом ошибки, содержащим информацию о проблеме: файл не найден или недостаточно полномочий.
Как правило, методы, использующие колбэк для обработки ошибок, не могут определить, насколько важна выявленная ошибка. Они возвращают ошибку пользователю для обработки. Важно контролировать поток содержимого колбэка, проверять функцию на наличие ошибки, прежде чем пытаться получить доступ к результату операции.
Чтобы использовать шаблон колбэка с первым аргументом-ошибкой в собственных асинхронных функциях, нужно принять функцию в качестве последнего аргумента и вызвать её:
function square(num, callback) {
if (typeof callback !== 'function') {
throw new TypeError(`Callback must be a function. Got: ${typeof callback}`);
}
// simulate async operation
setTimeout(() => {
if (typeof num !== 'number') {
// if an error occurs, it is passed as the first argument to the callback
callback(new TypeError(`Expected number but got: ${typeof num}`));
return;
}
const result = num * num;
// callback is invoked after the operation completes with the result
callback(null, result);
}, 100);
}
Любой вызывающий функцию square
должен пройти через колбэк, чтобы получить доступ к нужному результату или ошибке.
Не нужно непосредственно обрабатывать ошибку в функции колбэка. Её можно распространить вверх по стеку, передав на другой колбэк. Но сначала убедитесь, что вы не генерируете исключение внутри функции. Асинхронное исключение невозможно отследить, потому что окружающий блок try / catch
завершается до выполнения колбэка. Следовательно, исключение будет распространяться на вершину стека, что приведёт к завершению работы приложения. Исключение — когда обработчик зарегистрирован для process.on ('uncaughtException')
.
try {
square('8', (err, result) => {
if (err) {
throw err; // not recommended
}
console.log(result);
});
} catch (err) {
// This won't work
console.error("Caught error: ", err);
}
Отклонение обещаний
Обещания в JavaScript — это актуальный способ выполнения асинхронных операций в Node.js. Они предпочтительнее колбэков из-за лучшего потока, который соответствует современным способам анализа программ, особенно с шаблоном async / await
. Любой API-интерфейс Node.js, использующий колбэки с ошибкой для асинхронной обработки ошибок, может быть преобразован в обещания с помощью встроенного метода util.promisify ()
. Например, заставить метод fs.readFile ()
использовать обещания можно так:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
Переменная readFile
— это версия fs.readFile ()
с обещаниями, в которой отклонения обещаний используются для сообщения об ошибках. Эти ошибки можно отследить, связав метод catch
:
readFile('/path/to/file.txt')
.then((result) => console.log(result))
.catch((err) => console.error(err));
Также можно использовать обещанные API в функциях async
. Так выглядит основной способ использования обещаний в современном JavaScript: в нём код читается как синхронный, и для обработки ошибок применяют знакомый механизм try / catch
. Перед асинхронным запуском важно использовать await
, чтобы обещание было выполнено или отклонено до того, как функция возобновит выполнение. При отклонении обещания выражение await
выбрасывает отклонённое значение, которое впоследствии попадает в окружающий блок catch
.
(async function callReadFile() {
try {
const result = await readFile('/path/to/file.txt');
console.log(result);
} catch (err) {
console.error(err);
}
})();
Обещанияможно использовать в асинхронных функциях, возвращая обещание из функции и помещая код функции в обратный вызов обещания. Если есть ошибка, её стоит отклонить (reject) с помощью объекта Error. В противном случае можно разрешить (resolve) обещание с результатом, чтобы оно было доступно в цепочке метода .then
или напрямую как значение функции async
при использовании async / await
.
function square(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (typeof num !== 'number') {
reject(new TypeError(`Expected number but got: ${typeof num}`));
}
const result = num * num;
resolve(result);
}, 100);
});
}
square('8')
.then((result) => console.log(result))
.catch((err) => console.error(err));
Источники событий
Другой шаблон, подходящий для работы с длительными асинхронными операциями, которые могут приводить к множественным ошибкам или результатам, — это возврат EventEmitter
из функции и выдача события как для успешного, так и для неудачного случая:
const { EventEmitter } = require('events');
function emitCount() {
const emitter = new EventEmitter();
let count = 0;
// Async operation
const interval = setInterval(() => {
count++;
if (count % 4 == 0) {
emitter.emit(
'error',
new Error(`Something went wrong on count: ${count}`)
);
return;
}
emitter.emit('success', count);
if (count === 10) {
clearInterval(interval);
emitter.emit('end');
}
}, 1000);
return emitter;
}
Функция emitCount ()
возвращает новый эмиттер событий, который сообщает об успешном исходе в асинхронной операции. Она увеличивает значение переменной count
и каждую секунду генерирует событие успеха и событие ошибки, если значение count
делится на 4. Когда count
достигает 10, генерируется событие завершения. Этот шаблон позволяет передавать результаты по мере их поступления вместо ожидания завершения всей операции.
Вот как можно отслеживать и реагировать на каждое из событий, генерируемых функцией emitCount ()
:
const counter = emitCount();
counter.on('success', (count) => {
console.log(`Count is: ${count}`);
});
counter.on('error', (err) => {
console.error(err.message);
});
counter.on('end', () => {
console.info('Counter has ended');
});
Функция колбэка для каждого прослушивателя событий выполняется независимо, как только событие генерируется. Событие ошибки (error) — это особый случай для Node.js, потому что при отсутствии прослушивателя процесс Node.js выходит из строя. Вы можете закомментировать прослушиватель событий ошибки выше и запустить программу, чтобы увидеть, что произойдёт.
Расширение объекта ошибки
Необходимо создавать собственные пользовательские классы ошибок, чтобы лучше отражать разные типы ошибок: класс ValidationError
для ошибок, возникающих при проверке пользовательского ввода, класс DatabaseError
для операций с базами данных, TimeoutError
для операций, для которых истекло назначенное им время ожидания.
Пользовательские классы ошибок, расширяющие объект Error
, сохранят основные свойства ошибки: сообщение (message
), имя (name
) и стек (stack
). Но у них есть собственные свойства. ValidationError
можно улучшить, добавив значимые свойства — часть ввода, вызвавшую ошибку.
Вот как можно расширить встроенный объект Error
в Node.js:
class ApplicationError extends Error {
constructor(message) {
super(message);
// name is set to the name of the class
this.name = this.constructor.name;
}
}
class ValidationError extends ApplicationError {
constructor(message, cause) {
super(message);
this.cause = cause
}
}
Класс ApplicationError
— общая ошибка, а класс ValidationError
представляет любую ошибку, возникающую при проверке ввода данных пользователем. Он наследуется от класса ApplicationError
и дополняет его свойством cause
для указания ввода, вызвавшего ошибку. Пользовательские классы ошибки можно использовать, как и обычные:
function validateInput(input) {
if (!input) {
throw new ValidationError('Only truthy inputs allowed', input);
}
return input;
}
try {
validateInput(userJson);
} catch (err) {
if (err instanceof ValidationError) {
console.error(`Validation error: ${err.message}, caused by: ${err.cause}`);
return;
}
console.error(`Other error: ${err.message}`);
}
Ключевое слово instanceof
следует использовать для проверки конкретного типа ошибки. Не используйте имя ошибки для проверки типа, как в err.name === 'ValidationError'
: это не сработает, если ошибка получена из подкласса ValidationError
.
Типы ошибок
Типы ошибок можно разделить на две основные категории: ошибки программиста и операционные проблемы. К первому типу можно отнести неудачные или неправильные аргументы функции, в то время как временные сбои при работе с внешними API однозначно относятся ко второй категории.
Операционные ошибки
Операционные ошибки — это предсказуемые ошибки, которые возникают в процессе выполнения приложения. Это не обязательно баги, чаще это даже внешние обстоятельства, способные нарушить ход выполнения программы. В таких случаях можно полностью понять влияние ошибки на процессы:
-
Запрос API не выполняется по какой-либо причине (например, сервер не работает или превышен лимит скорости).
-
Соединение с базой данных потеряно, например, из-за неисправного сетевого соединения.
-
ОС не может выполнить запрос на открытие файла или запись в него.
-
Пользователь отправляет на сервер недопустимые данные: неверный номер телефона или адрес электронной почты.
Ошибки программиста
Ошибки программиста — это ошибки в логике или синтаксисе программы, которые можно исправить только путём изменения исходного кода. Ошибки этого типа невозможно обработать, потому что это недочёты в программе:
-
Синтаксические ошибки: незакрытая фигурная скобка.
-
Ошибки типа при попытке сделать что-то неправильное: выполнение операций с операндами несовпадающих типов.
-
Неверные параметры при вызове функции.
-
Ссылки на ошибки при неправильном написании имени переменной, функции или свойства.
-
Попытка получить доступ к местоположению за концом массива.
-
Неспособность обработать операционную ошибку.
Обработка операционных ошибок
Операционные ошибки в большинстве случаев предсказуемы. Их обработка — это рассмотрение вероятности неудачного завершения операции, возможных причин и последствий. Рассмотрим несколько стратегий обработки операционных ошибок в Node.js.
Сообщить об ошибке в стек
Во многих случаях лучше остановить выполнение программы, очистить все незавершённые процессы и сообщить об ошибке в стек. Зачастую это единственный способ исправить ошибку, когда функция, в которой она возникла, находится дальше по стеку.
Повторить операцию
Сетевые запросы к внешним службам иногда могут завершаться ошибкой, даже если запрос полностью верен. Это случается из-за сбоя и неполадках сети или перегрузке сервера. Можно повторить запрос несколько раз, пока он не будет успешно завершён или пока не будет достигнуто максимальное количество повторных попыток. Первое, что нужно сделать, — это определить, уместно ли повторить запрос. Если исходный код состояния HTTP ответа — 500, 503 или 429, повторте запрос через некоторое время.
Проверьте, присутствует ли в ответе HTTP-заголовок Retry-After. Он указывает на точное время ожидания перед выполнением последующего запроса. Если его нет, необходимо отложить последующий запрос и постепенно увеличивать временной промежуток для каждой повторной попытки. Этот метод известен как стратегия экспоненциального отката. Нужно ещё определить максимальное время задержки и число запросов до отказа от дальнейших попыток.
Отправить ошибку клиенту
По умолчанию пользователи вводят данные неправильно. Поэтому первое, что нужно сделать перед запуском каких-либо процессов, — проверить введённые данные и незамедлительно сообщить пользователю о любых ошибках. При обработке ошибок клиента обязательно включите всю информацию, необходимую для создания сообщения об ошибке и имеющую смысл для пользователя.
Прервать программу.
В случае неисправимых системных ошибок разумный выход — зарегистрировать ошибку и немедленно завершить работу программы. Если исключение невозможно исправить на уровне JavaScript, то, возможно, не получится корректно завершить работу сервера. Тогда нужен системный администратор, способный всё исправить.
Предотвращение ошибок программиста
Ошибки программиста сами по себе не могут быть обработаны, потому что их причина в коде или в логике. Однако ошибаться можно реже.
Принять TypeScript
TypeScript — это строго типизированное надмножество JavaScript. Основная цель его проектирования — статическая идентификация потенциально ошибочных конструкций без штрафных санкций во время выполнения. Принимая TypeScript в проекте (с максимально возможными параметрами компилятора), можно устранить целый класс ошибок программиста в ходе компиляции.
Когда проект на TypeScript, такие ошибки, как undefined
is not a function
, синтаксические или ссылочные ошибки, исчезают из кодовой базы. Перенос на TypeScript можно выполнять постепенно. Для быстрой миграции есть инструмент ts-migrate.
Определить поведение для неверных параметров
Многие ошибки возникают из-за передачи неверных параметров. Это может быть связано не только с очевидными ошибками, такими как передача строки вместо числа, но и с небольшими погрешностями, когда аргумент функции имеет правильный тип, но выходит за пределы диапазона, который функция способна обработать. Когда функция вызывается таким образом, она может выдать неверное значение, например NaN
. Когда сбой обнаруживается, сперва трудно определить его причину.
При работе с неверными параметрами и определяйте их поведение, либо выдавая ошибку, либо возвращая специальное значение, такое как null
, undefined
или -1
, когда проблема может быть решена локально. Первый вариант— это подход, используемый JSON.parse ()
, который выдаёт исключение SyntaxError
, если строка для синтаксического анализа недействительна. Второй вариант — метод string.indexOf ()
.
Автоматизированное тестирование
Автоматизированные наборы тестов повышает вероятность исправления ошибок. Тесты помогают выяснить, как функция работает с нетипичными значениями. Для модульного тестирования подходят среды, такие как Jest или Mocha.
Неперехваченные исключения и необработанные отклонения обещаний
Неперехваченные исключения и необработанные отклонения обещаний вызываются ошибками программиста. Событие uncaughtException
генерируется, когда исключение не перехватывается до того как достигнет цикла обработки событий. При обнаружении неперехваченного исключения приложение немедленно выходит из строя. Для переопределения такого поведения всегда можно добавить обработчик события:
// unsafe
process.on('uncaughtException', (err) => {
console.error(err);
});
Но неперехваченное исключение указывает на то, что приложение находится в неопределённом состоянии. Поэтому попытка возобновить работу в обычном режиме без восстановления после ошибки небезопасна и может привести к утечке памяти и зависанию сокетов. Лучше использовать обработчик uncaught Exception
для очистки всех выделенных ресурсов, закрытия соединений и ведения лога ошибок для оценки перед завершением процесса.
// better
process.on('uncaughtException', (err) => {
Honeybadger.notify(error); // log the error in a permanent storage
// attempt a gracefully shutdown
server.close(() => {
process.exit(1); // then exit
});
// If a graceful shutdown is not achieved after 1 second,
// shut down the process completely
setTimeout(() => {
process.abort(); // exit immediately and generate a core dump file
}, 1000).unref()
});
Событие unhandledRejection
генерируется, когда отклонённое обещание не обрабатывается блоком catch
. В отличие от uncaughtException
, эти события не вызывают немедленного сбоя приложения. Однако необработанные отклонения обещаний сейчас признаны устаревшими и могут немедленно завершить процесс в следующих релизах Node.js. Отслеживать необработанные отклонения обещаний можно с помощью прослушивателя событий unhandledRejection
:
process.on('unhandledRejection', (reason, promise) => {
Honeybadger.notify({
message: 'Unhandled promise rejection',
params: {
promise,
reason,
},
});
server.close(() => {
process.exit(1);
});
setTimeout(() => {
process.abort();
}, 1000).unref()
});
Серверы необходимо запускать с помощью диспетчера процессов, который автоматически перезапустит их в случае сбоя. Распространённый вариант — PM2, но для Linux существуют также systemd
и upstart
, а пользователи Docker могут использовать собственную политику перезапуска. По завершении всех процессов стабильное обслуживание будет восстановлено почти мгновенно, а у вас будт информация о неперехваченном исключении. Можно запутсить несколько процессов и применить балансировщик нагрузки для распределения входящих запросов. Это поможет предотвратить простои.
Централизованная отчётность об ошибках
Ни одна стратегия обработки ошибок не будет полной без надёжной стратегии ведения журнала ошибок. Когда происходит сбой, важно узаписать как можно больше информации о проблеме. Централизация логов позволяет оценить, что происходит в коде.
Honeybadger предоставляет всё необходимое для отслеживания ошибок. Интегрируется так:
Установите пакет
Используйте npm для установки пакета:
$ npm install @honeybadger-io/js --save
Импортируйте библиотеку
Импортируйте библиотеку и настройте её с помощью ключа API, чтобы получать сообщения об ошибках:
const Honeybadger = require('@honeybadger-io/js');
Honeybadger.configure({
apiKey: '[ YOUR API KEY HERE ]'
});
Сообщите об ошибках
Метоодом notify ():
try {
// ...error producing code
} catch(error) {
Honeybadger.notify(error);
}
Просмотрите полную документацию или ознакомьтесь с образцом Node.js / Express на GitHub.
Без обработки ошибок не бывает надёжного софта.
Спасибо за внимание и удачного кода!
Обработка пользовательских ошибок, расширение Error
Здравствуйте! Иногда при разработке часто необходимы собственные классы ошибок для различных вещей, которые могут пойти не так в наших программах. Для ошибок при работе с сетью может понадобиться класс HttpError, для операций с базой данных класс DbError,а для поиска – класс NotFoundError и т.д.
Такие ошибки в JavaScript должны поддерживать базовые свойства, такие как message, name и stack. Но также они могут иметь и свои собственные свойства. Например, объекты HttpError могут иметь такое свойство statusCode со значениями соотвественно 404, 403 или 500.
Язык JavaScript позволяет вызывать метод throw с любыми аргументами, то есть технически классы ошибок не нуждаются в наследовании от класса Error. Но если использовать наследование, то появляется возможность идентификации объектов ошибок посредством obj instanceof Error. Так что лучше конечно же применять наследование.
По мере развития приложения, наши ошибки образуют иерархию, например, класс HttpTimeoutError может наследовать от класса HttpError ну и так далее.
Расширение Error
В качестве примера давайте рассмотрим функцию readUser(json), которая должна читать данные пользователя в формате JSON.
Пример того, как может выглядеть корректный json:
let json = `{ "name": "John", "age": 30 }`;
Внутри будем использовать метод JSON.parse. При получении некорректного json он будет генерировать такую ошибку SyntaxError. Но даже если json синтаксически верен, то это не значит, что это будет корректный пользователь, правильно? Могут быть пропущены некоторые данные. Например, могут отсутствовать свойства name и age, которые являются необходимыми для пользователей.
Наша функция readUser(json) будет не только читать JSON-данные, но и валидировать их. Если необходимые поля отсутствуют или данные в неверном формате, то это будет ошибкой. Но не синтаксической ошибкой SyntaxError, потому что данные синтаксически корректны. Это будет ошибка другого рода.
Назовём её ошибкой валидации ValidationError и создадим для неё отдельный класс. Ошибка этого вида должна содержать информацию о поле, которое и является источником ошибки.
Наш класс ValidationError должен наследоваться от встроенного класса Error.
Класс Error встроенный, вот его примерный код, просто чтобы мы знали, что расширяем:
// "Псевдокод" встроенного класса Error, определённого самим JavaScript class Error { constructor(message) { this.message = message; this.name = "Error"; // (разные имена для разных встроенных классов ошибок) this.stack = <стек вызовов>; // нестандартное свойство, но обычно поддерживается } }
Теперь давайте унаследуем от него ValidationError и попробуем новый класс в действии:
class ValidationError extends Error { constructor(message) { super(message); // (1) this.name = "ValidationError"; // (2) } } function test() { throw new ValidationError("Упс!"); } try { test(); } catch(err) { alert(err.message); // Упс! alert(err.name); // ValidationError alert(err.stack); // список вложенных вызовов с номерами строк для каждого }
Обратите внимание: в строке 1 мы вызываем родительский конструктор. JavaScript требует от нас вызова метода super в дочернем конструкторе, так что это обязательно. Родительский конструктор устанавливает свойство message.
Родительский конструктор также устанавливает свойство name для «Error», поэтому в строке 2 мы сбрасываем его на правильное значение.
Попробуем использовать его в функции readUser(json):
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } // Использование function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("Нет поля: age"); } if (!user.name) { throw new ValidationError("Нет поля: name"); } return user; } // Рабочий пример с try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Некорректные данные: " + err.message); // Некорректные данные: Нет поля: name } else if (err instanceof SyntaxError) { // (*) alert("JSON Ошибка Синтаксиса: " + err.message); } else { throw err; // неизвестная ошибка, пробросить исключение (**) } }
Блок try..catch в коде выше обрабатывает и нашу ValidationError, и встроенную SyntaxError из функции JSON.parse.
Обратите внимание, как мы используем метод instanceof для проверки конкретного типа ошибки в строке (*).
Мы можем также проверить тип, используя err.name:
// ... // вместо (err instanceof SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
Версия с instanceof гораздо лучше, потому что в будущем мы ведь собираемся расширить класс ValidationError, сделав его подтипы, такие как класс PropertyRequiredError. И проверка instanceof продолжит работать для новых наследованных классов. Так что это задел на будущее.
Также важно, что если catch встречает неизвестную ошибку, то он пробрасывает её в строке (**). Блок catch знает, только как обрабатывать ошибки валидации и синтаксические ошибки, а другие виды ошибок (из-за опечаток в коде и другие непонятные) он должен выпустить соответственно наружу.
Дальнейшее наследование
Класс ValidationError является слишком уж общим. Много чего может пойти не так. Свойство может отсутствовать или иметь неверный формат (например, строка как значение возраста age). Поэтому для отсутствующих свойств сделаем более конкретный класс PropertyRequiredError. Он будет нести дополнительную информацию о свойстве, которое отсутствует.
class ValidationError extends Error { constructor(message) { super(message); this.name = "ValidationError"; } } class PropertyRequiredError extends ValidationError { constructor(property) { super("Нет свойства: " + property); this.name = "PropertyRequiredError"; this.property = property; } } // Применение function readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } return user; } // Рабочий пример с try..catch try { let user = readUser('{ "age": 25 }'); } catch (err) { if (err instanceof ValidationError) { alert("Неверные данные: " + err.message); // Неверные данные: Нет свойства: name alert(err.name); // PropertyRequiredError alert(err.property); // name } else if (err instanceof SyntaxError) { alert("Ошибка синтаксиса JSON: " + err.message); } else { throw err; // неизвестная ошибка, повторно выбросит исключение } }
Новый класс PropertyRequiredError очень просто использовать: необходимо указать лишь имя свойства new PropertyRequiredError(property). Сообщение для пользователя message генерируется конструктором.
Обратите внимание, что свойство this.name в конструкторе класса PropertyRequiredError снова присвоено вручную. Правда, немного утомительно – присваивать свойство this.name = <class name> в каждом классе пользовательской ошибки. Можно этого избежать, если сделать наш собственный класс ошибки, который будет ставить this.name = this.constructor.name. И затем наследовать все ошибки уже от него.
Давайте назовём его как MyError.
Вот упрощённый код с MyError и другими пользовательскими классами ошибок:
class MyError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } class ValidationError extends MyError { } class PropertyRequiredError extends ValidationError { constructor(property) { super("Нет свойства: " + property); this.property = property; } } // name корректное alert( new PropertyRequiredError("field").name ); // PropertyRequiredError
Теперь пользовательские ошибки стали намного короче, особенно ValidationError, так как мы избавились от строки «this.name = …» в конструкторе класса.
Обёртывание исключений
Назначение функции readUser в приведённом выше коде – это собственно «чтение данных пользователя». В процессе могут возникнутьи различные виды ошибок. Сейчас у нас есть SyntaxError и ValidationError, но в будущем функция readUser может расшириться и, возможно, генерировать другие типы ошибок.
Код, который вызывает readUser, должен обрабатывать все эти ошибки.
Сейчас в нём используются проверки if в блоке catch, которые проверяют класс и обрабатывают известные ошибки и пробрасывают дальше все неизвестные. Но если функция readUser генерирует несколько видов ошибок, то мы должны спросить себя: действительно ли мы хотим проверять все типы ошибок поодиночке во всех местах в коде, где вызывается функция readUser?
Часто ответ «Нет»: внешний код хочет быть на один уровень выше всего этого. Он хочет иметь какую-то обобщённую ошибку чтения данных. Почему именно это произошло – часто не имеет значения (об этом говорится в сообщении об ошибке). Или даже лучше, если есть способ получить подробности об ошибке, но только если нам это нужно.
Итак, давайте создадим новый класс ReadError для представления таких ошибок. Если ошибка возникает внутри readUser, мы её перехватим и сгенерируем ReadError. Мы также сохраним ссылку на исходную ошибку в свойстве cause. Тогда внешний код должен будет только проверить наличие ReadError.
Этот код определяет ошибку ReadError и демонстрирует её использование в readUserи try..catch:
class ReadError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = 'ReadError'; } } class ValidationError extends Error { /*...*/ } class PropertyRequiredError extends ValidationError { /* ... */ } function validateUser(user) { if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } } function readUser(json) { let user; try { user = JSON.parse(json); } catch (err) { if (err instanceof SyntaxError) { throw new ReadError("Синтаксическая ошибка", err); } else { throw err; } } try { validateUser(user); } catch (err) { if (err instanceof ValidationError) { throw new ReadError("Ошибка валидации", err); } else { throw err; } } } try { readUser('{bad json}'); } catch (e) { if (e instanceof ReadError) { alert(e); // Исходная ошибка: SyntaxError:Unexpected token b in JSON at position 1 alert("Исходная ошибка: " + e.cause); } else { throw e; } }
В приведённом выше коде функция readUser работает так, как описано – распознаёт синтаксические ошибки и ошибки валидации и выдаёт вместо них ошибки ReadError (неизвестные ошибки, как обычно, пробрасываются).
Внешний код проверяет только instanceof ReadError. Не нужно перечислять все возможные виды ошибок
Этот подход называется «обёртывание исключений», потому что мы берём «исключения низкого уровня» и «оборачиваем» их в ReadError, который является более абстрактным и более удобным для использования в вызывающем коде. Такой подход широко используется в объектно-ориентированном программировании.
Итого
- Мы можем наследовать свои классы ошибок от Error и других встроенных классов ошибок, но нужно позаботиться о свойстве name и не забыть вызвать метод super.
- Мы можем использовать instanceof для проверки типа ошибок. Это также работает и с наследованием. Но иногда у нас объект ошибки, возникшей в сторонней библиотеке, и нет простого способа получить класс. Тогда для проверки типа ошибки можно использовать свойство name.
- Обёртывание исключений является распространённой техникой: функция ловит низкоуровневые исключения и создаёт одно «высокоуровневое» исключение вместо низкоуровневых. Иногда низкоуровневые исключения становятся свойствами этого объекта, как err.cause в примерах выше, но это вовсе не обязательно.
Задачи
Наследование от класса SyntaxError
Создайте класс FormatError, который наследует от встроенного класса SyntaxError.
Класс должен поддерживать свойства message, name и stack.
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
Поделиться
Твитнуть
Поделиться
(Visited 258 times, 1 visits today)
In the course of developing something, it is necessary to have own error classes for reflecting particular things that can go wrong. For errors in network operations, HttpError is necessary, for searching operations-NotFoundError, for database operations- DbError, and more. They should support basic error properties such as name, message, and stack. Also, they can include other properties. For example, HttpError objects can contain a statusCode such as 403, 404, or 500.
JavaScript allows using throw with any argument. So, technically, custom errors don’t need inheritance from Error.
Now, let’s discover what extending error is. To be more precise, we can start from a function readUser(json), which reads JSON with user data.
So, a valid json looks like this:
let json = `{ "name": "John", "age": 25 }`;
Internally, JSON.parse is used.
In case it gets malformed json then it throws SyntaxError. Even if json is syntactically correct it doesn’t consider that the user is valid. It can miss essential data such as name, age, or other properties.
The readUser(json) function both reads JSON and checks the data. In case the format is wrong, then an error occurs. As the data is syntactically correct, it’s not a SyntaxError. It is a ValidationError.
For ValidationError it is necessary to inherit from the built-in Error class.
Here is the code for extending:
// "Pseudocode" for the built-in Error class
class Error {
constructor(message) {
this.message = message;
this.errorName = "Error"; // different names for different built-in error classes
this.stack = < call stack > ; // non-standard, but most environments support it
}
}
In the example below, ValidationError is inherited from it:
Javascript ValidationError class
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = «ValidationError»;
}
}
function testFunc() {
throw new ValidationError(«Oops»);
}
try {
testFunc();
} catch (err) {
console.log(err.message); // Oops
console.log(err.name); // ValidationError
console.log(err.stack); // a list of nested calls, with line numbers for each
}
Now, let’s try using it in createUser(json), like this:
Javascript ValidationError class
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = «ValidationError»;
}
}
function createUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new ValidationError(«No field: age»);
}
if (!user.name) {
throw new ValidationError(«No field: name»);
}
return user;
}
try {
let user = createUser(‘{ «age»: 20 }’);
} catch (err) {
if (err instanceof ValidationError) {
console.log(«Invalid data: » + err.message); // Invalid data: No field: name
} else if (err instanceof SyntaxError) {//(*)
console.log(«JSON Syntax Error: » + err.message);
} else {
throw err; // unknown error, rethrow it
}
}
So, in the code above the try..catch block handles ValidationError and SyntaxError from JSON.parse.
It is especially interesting how instanceof is used for checking a particular error in the (*) line.
The err.name can also look like here:
// ...
// instead of, err instanceof SyntaxError
} else if (err.name == "SyntaxError") { // (*)
}
However, the version with instanceof is better, as the next step is to extend ValidationError, making subtypes of it. The instanceof check will keep working for new inheriting classes.
The ValidationError class is very universal. Hence, various things can go wrong. For example, a property can be in a wrong format or it can be absent.
Let’s consider the PropertyRequiredError for absent properties. It carries extra information about the missing property. Here is how it looks like:
Javascript ValidationError and PropertyRequiredError class
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = «ValidationError»;
}
}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super(«Not a property: » + property);
this.name = «PropertyRequiredError»;
this.property = property;
}
}
function createUser(json) {
let user = JSON.parse(json);
if (!user.age) {
throw new PropertyRequiredError(«age»);
}
if (!user.name) {
throw new PropertyRequiredError(«name»);
}
return user;
}
// Example with try..catch
try {
let user = createUser(‘{ «age»: 20 }’);
} catch (err) {
if (err instanceof ValidationError) {
console.log(«Invalid data: » + err.message); // Invalid data: No property: name
console.log(err.name); // PropertyRequiredError
console.log(err.property); // name
} else if (err instanceof SyntaxError) {
console.log(«JSON Syntax Error: » + err.message);
} else {
throw err; // unknown error, rethrow it
}
}
The new class PropertyRequiredError is easy in usage. All you need is to do is passing the property name: new PropertyRequiredError(property). The constructor creates a human-readable message.
Please, consider that this.name in PropertyRequiredError constructor is assigned manually. So, assigning this.name = <class name> in each custom error class. For avoiding that the basic error class, which assigns this.name = this.constructor.name is made. It can inherit all the custom errors. It can be called ErrorName.
Here it is and other custom error classes:
Javascript ValidationError and PropertyRequiredError class
class ErrorName extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends ErrorName {}
class PropertyRequiredError extends ValidationError {
constructor(property) {
super(«Not a property: » + property);
this.property = property;
}
}
// correct name
console.log(new PropertyRequiredError(«field»).name); // PropertyRequiredError
So, now custom errors are shorter.
The purpose of the createUser function is reading the user data. In the process, various errors may occur.
The code calling createUser must handle the errors. In the example below, it applies multiple ifs in the catch block, checking the class and handling known errors and rethrowing the unknown ones.
The scheme will look as follows:
In the code above, there are two kinds of errors. Of course, they can be more.
The technique, represented in this part is known as “wrapping extensions”.
Let’s start at making a new class ReadError to present a generic data reading error. The readUser function catches the data reading errors, occurring inside it (for example, ValidationError and SyntaxError) and create ReadError uthuinstead.
The ReadError object keeps the reference to the original error inside the cause property.
try {
//...
createUser() // the potential error source
//...
}
catch (err) {
if (err instanceof ValidationError) {
// handle validation error
} else if (err instanceof SyntaxError) {
// handle syntax error
} else {
throw err; // unknown error, rethrow it
}
}
Let’s take a look at the code, which specifies ReadError demonstrating its usage in readUser and try..catch:
Javascript try…catch error handling
class ReadError extends Error {
constructor(message, cause) {
super(message);
this.cause = cause;
this.name = ‘ReadError’;
}
}
class ValidationError extends Error { /*…*/ }
class PropertyRequiredError extends ValidationError { /* … */ }
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError(«age»);
}
if (!user.name) {
throw new PropertyRequiredError(«name»);
}
}
function createUser(json) {
let user;
try {
user = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError(«Syntax Error», err);
} else {
throw err;
}
}
try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError(«Validation Error», err);
} else {
throw err;
}
}
}
try {
createUser(‘{bad json}’);
} catch (e) {
if (e instanceof ReadError) {
console.log(e);
// Error: SyntaxError:
// Unexpected token b in JSON at position 1
console.log(«Error: » + e.cause);
} else {
throw e;
}
}
The approach, described above is named “wrapping exceptions” as “low-level” exceptions are taken and wrapped into ReadError, which is more abstract.
In brief, we can state that, normally, it is possible to inherit from Error and other built-in classes. All you need is to take care of the name property, not forgetting to call super.
The instanceof class can also be applied to check for specific errors. It also operates with inheritance. At times when an error object comes from a third-party library, the name property can be used.
A widespread and useful technique is wrapping exceptions. With it, a function can handle low-level exceptions, creating high-level errors, instead of different low-level ones.