Если вы пришли сюда только ради ответа и вам не интересны рассуждения - листайте вниз :)
Для начала, давайте вспомним, а как вообще ловят ошибки в js, будь то браузер или сервер. В js есть конструкция try...catch
.
try {
let data = JSON.parse('...');
} catch(err: any) {
// если произойдет ошибка, то мы окажемся здесь
}
Это общепринятая конструкция и в большинстве языков она есть. Однако, тут есть проблема (и как окажется дальше - не единственная), эта конструкция "не будет работать" для асинхронного кода, для кода который был лет 5 назад. В те времена, в браузере использовали для Ajax запроса XMLHttpRequest
.
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com', true);
xhr.addEventListener('error', (e: ProgressEvent<XMLHttpRequestEventTarget>) => {
// если произойдет ошибка, то мы окажемся здесь
});
Тут используется механизм подписки на событие возникновения ошибки. В данном случае, переменная e
является событием, фактически мы ушли от настоящей ошибки и закрылись некоторой абстракцией, за которой спрятана настоящая ошибка, доступа к которой у нас нет.
В NodeJS с самого начала продвигалась концепция Error-First Callback, эта идея применялась для асинхронных функций, например, для чтения файла. Смысл ее в том, чтобы первым аргументом передавать в функцию обратного вызова ошибку, а следующими аргументами уже получаемые данные.
import fs from 'fs';
fs.readFile('file.txt', (err, data) => {
if (err) {
// обработка ошибки
}
// если все хорошо, работаем с данными
});
Если мы посмотрим какой тип имеет переменная err
, то увидим следующее:
interface ErrnoException extends Error {
errno?: number | undefined;
code?: string | undefined;
path?: string | undefined;
syscall?: string | undefined;
}
Тут действительно находится ошибка. По сути, это тот же способ, что и выше, только в этом случает мы получаем объект Error
.
Через некоторое время, в Javascript появились Promise
. Они, безусловно, изменили разработку на js к лучшему. Ведь никто* никто не любит городить огромные конструкции из функций обратного вызова.
fetch('https://api.example.com')
.then(res => {
// если все хорошо, работаем с данными
})
.catch(err => {
// обработка ошибки
});
Несмотря на то, что внешне этот пример сильно отличается от первого, тем не менее, мы видим явную логическую связь. Очевидно, что разработчики хотели сделать похожую на try...catch
конструкцию. Со временем, появился еще один способ обработать ошибку в асинхронном коде. Этот способ, по сути, является лишь синтаксическим сахаром для предыдущего примера.
try {
const res = await fetch('https://api.example.com');
// если все хорошо, работаем с данными
} catch(err) {
// обработка ошибки
}
Также, конструкция try...catch
позволяет ловить ошибки из нескольких промисов одновременно.
try {
let usersRes = await fetch('https://api.example.com/users');
let users = await usersRes.json();
let chatsRes = await fetch('https://api.example.com/chats');
let chats = await chatsRes.json();
// если все хорошо, работаем с данными
} catch(err) {
// обработка ошибки
}
Вот, замечательный вариант ловли ошибок. Любая ошибка которая возникнет внутри блока try
, попадет в блок catch
и мы точно её обработаем.
Действительно, а правда ли, что мы обработаем ошибку, или всего лишь сделаем вид? На практике, скорее всего, возникнувшая ошибка будет просто выведена в консоль или т.п. Более того, при появлении ошибки*, интерпретатор прыгнет в блок catch
, где не мы, не TypeScript не сможет вывести тип переменной, попавшей туда (пример - возврат с помощью Promise.reject
), после чего, произойдет выход из функции. То есть, мы не сможем выполнить код который находится в этом же блоке, но который расположен ниже функции, внутри которой произошла ошибка. Конечно, мы можем предусмотреть такие ситуации, но сложность кода и читаемость вырастут многократно.
Давайте попробуем использовать подход, предлагаемый разработчиками одного небезызвестного языка.
let [users, err] = await httpGET('https://api.example.com/users');
if (err !== null) {
// обработка ошибки
}
// продолжаем выполнение кода
Возможную ошибку мы держим всегда рядом с данными, возвращаемыми из функции, что намекает нам на то, что переменную err
желательно проверить.
Пример для вызова нескольких функций возвращающих Promise
.
let err: Error,
users: User[],
chats: Chat[];
[users, err] = await httpGET('https://api.example.com/users');
if (err !== nil) {
// обработка ошибки
}
[chats, err] = await httpGET('https://api.example.com/chats');
if (err !== nil) {
// обработка ошибки
}
Конечно, мы можем, как и прежде, просто выходить из функций при появлении ошибки, но если, все таки, появляется необходимость отнестись к коду более ответственно, мы без труда можем начать это делать.
Давайте рассмотрим как можно реализовать такую функцию и что нам вообще нужно делать. Для начала, давайте определим тип PairPromise
. В данном случае, я решил использовать null
если результата или ошибки нету, так как он просто короче.
type PairPromise<T> = Promise<[T, null] | [null, Error]>;
Определим возможные возвращаемые ошибки.
const notFoundError = new Error('NOT_FOUND');
const serviceUnavailable = new Error('SERVICE_UNAVAILABLE');
Теперь опишем нашу функцию.
const getUsers = async (): PairPromise<User[]> => {
try {
let res = await fetch('https://api.example.com/users');
if (res.status === 504) {
return Promise.resolve([null, serviceUnavailable]);
}
let users = await res.json() as User[];
if (users.length === 0) {
return Promise.resolve([null, notFoundError]);
}
return Promise.resolve([users, null]);
} catch(err) {
return Promise.resolve([null, err]);
}
}
Пример использования такой функции.
let [users, err] = await getUsers();
if (err !== null) {
switch (err) {
case serviceUnavailable:
// сервис недоступен
case notFoundError:
// пользователи не найдены
default:
// действие при неизвестной ошибке
}
}
Вариантов применения данного подхода обработки ошибок очень много. Мы сочетаем удобства конструкции try...catch
и Error-First Callback, мы гарантированно поймаем все ошибки и сможем удобно их обработать, при необходимости. Как приятный бонус - мы не теряем типизацию. Также, мы не скованы лишь объектом Error
, мы можем возвращать свои обертки и успешно их использовать, в зависимости от наших убеждений.
Очень интересно мнение сообщества на эту тему.
… и что гарантирует, что возвращенная ошибка действительно будет обработана, а не будет так, что вызывающий код просто ее отбросит и вернет
users
?Тут может помочь TS: пока не будет проверен тип
err
, типusers
будетUsers | null
НапримерДействительно, ничто не может заставить разработчика обязательно обрабатывать все ошибки. Более того, такой способ позволяет не извлекать ошибку вообще. Но, такой код стимулирует разработчика озаботиться обработкой ошибки. В случае с try…catch, по опыту, почти все кейсы будут опущены…(
Не понимаю, каким образом.
Что значит "опущены"? В случае с эксепшном, если нет
catch
, разве ошибка не будет брошена в вызывающем коде?Такой подход позволят узнать об ошибке «не отходя от кассы», как говориться. Более того, это банально лучше читается да и отладка такого кода, будет куда приятнее, без прыжков по коду, линейно.
Позволяет узнать — да. Но как это стимулирует их обрабатывать?
А точно это лучше читается? Я вот предпочитаю видеть в первую очередь обработку основного сценария, а не ошибок.
Обработка ошибок должна быть неотъемлемой частью сценария, иначе, как всегда, обработка будет пропущена, как и написание тестов, я полагаю…
Почему обработка ошибок вида "у меня тут сеть недоступна" должна быть неотъемлемой частью бизнес-сценария?
Есть сильно больше одного случая, когда достаточно прервать выполнение, отрапортовать ошибку наверх, а супервизор сам разберется.
Только проблема в раскрытии имплементации бизнес логики... если планируется обрабатывать "у меня тут сеть недоступна" не как базовый Exception, то стоит делать обертку и это будет неотъемлемой частью логики. Убирать неявное - хорошая практика.
Если планируется — то будет. А если не планируется, то не будет, и можно просто не писать лишнего кода.
Вариант с Go'шной обработкой ошибок, заставляет нас проверить на ошибки результат. Но с той же легкостью мы можем этого и не делать)
Ленивые разработчики будут писать что-то типа
[chats] = await httpGET('https://api.example.com/chats');
Но на CodeReview такие косяки будет заметнее, и это хорошо.
А вот что плохо, так это загрязнение кода от этих постоянных проверок.
Есть ФП подход к обработке ошибок, он сложнее, но если привыкнуть то гуд.
https://habr.com/ru/post/457098/
Да, такой подход позволяет опускать ошибки (как и в го, кстати), но позволяет меньше отвлекаться на этапе прототипирования.
По поводу «плохо» - если может возникнуть ошибка, то она возникнет) Это первое. Если мы не хотим проверять ошибки(загрязнять код(c)), то можно как в случае с try…catch замести все под коврик и просто вывести пользователю “sorry…”
Несмотря на то, что даже в MDN фигурирует именно слово "ошибки", мне кажется более уместным всё-таки называть это исключениями. В англ. оригинале как раз используется слово "exception".
UPD: Имею в виду, что ошибки бывают восстановимыми и невосстановимыми. Во втором случае, если не делать try/catch, то программа падает и это называется исключением. А в конце этой статьи как раз приводится пример работы с восстановимыми ошибками, что в последнее время считается более правильным.
На самом деле, в реальном (более низком уровне) мире, граница между ошибкой и исключением очень размыта.
Позанудствую.
Нулевая длина списка вряд ли должна описываться ошибкой notFoundError. Ведь коллекция `users` есть. Если бы пользователь пошёл по пути `/users/1/`, а пользователя c `id = 1` нет, то тогда можно отдать notFoundError.
Из async-функции разве не достаточно возвращать просто массив `[data, error]` без оборачивания в
Promise.resolve
?Достаточно
Так написано для большей наглядности. Да и мне так больше нравится (субъективно).
Что касается ‘not found’ , действительно, это притянутый за уши пример, тут может быть любая другая ошибка.
Далеко не всегда нужно обрабатывать ошибки и очень часто ошибка, брошенная через throw вполне себе может привести к 500 ошибке и записана куда-нибудь в лог и это будет правильно.
Все удобство в try..catch в том, чтобы отловить только те ошибки, на которые ты должен как-то специфически отреагировать. А остальные - ну а как ты их нормально обработаешь? Пусть себе ловит какой-то общий обработчик, который запишет в лог, а юзеру скажет сорян. Увидел в логе необработанную ошибку, понял, что такой ситуации можно избежать — делаешь catch и именно ее и ловишь, как-то так ????♂️
Для описанного вами случая можно использовать “switch default”
А зачем, когда я просто не хочу смотреть на ошибку ни на одном из уровней, которых может быть дофига
Такой способ позволяет обработать разумные ошибки, а не просто написать в лог. Но действительно, фронтенд приучает людей писать код расслаблено, просто поймать ошибку через catch и больше ничего не делать. На практике, в сложных системах, это более чем разумно. Не всегда нам нужно просто прекращать идти по сценарию. Представь, что у нас есть много сервисов и любой из них может «пропасть» в любой момент, это не повод прекращать работу всего проекта.
Прекрасно представляю, если жить без сервиса можно - вполне себе ок обработать ошибки от него. Если важен и без него никак (обычно это как раз такие) - то сорян, никакой обработки делать не нужно, пусть ловится глобально.
Все это надо держать в балансе, заставлять разработчика проверять ошибки всегда — неправильно
… а обычный catch не позволяет?
Позволяет!
Судя по всем показателям (комменты, карма, рейтинг), люди восприняли мой текст, как призыв все бросить и безоговорочно начать использовать такой подход вообще везде. Вероятно, виноват мой стиль неопытного повествователя (первая публикация). Но идея такого подхода, фактически, не заменить, но дополнить стандартный подход. Всего навсего, ещё один инструмент, для некоторых ситуаций. Действительно, если логика приложения не требует такого подхода, то и использовать его не обязательно.
Когда в системе два подхода к обработке ошибок, программисты начинают их путать.
Я выше приводил примеры такого как ошибки обрабатываются в случает XMLHttpRequest и readFile, в первом случае - подписка на событие ошибки, второй - ошибка передаётся первым аргументом в колбеке. Вообще забавна такая реакция сообщества, такое ощущение, что никто не знает про вышеописанные способы/приёмы. Вариант который предлагаю я, по сути, синтаксический сахар для ErrorFirstCallback. С трудом верится, что люди не знают такие вещи, видимо, такие люди просто более активны в комментариях…
Это называется "легаси". И это как раз и неудобно, что для обработки ошибок используются разные подходы.
Подход у XMLHttpRequest - показывает, что жизнь может быть чуть сложнее. С readFile, действительно, сейчас есть возможность использовать вариант с промисом. Но старый вариант позволял выполнить определенный код не теряя "контекст".
Так это неудобно же. Наличие нескольких разных способов обработать ошибку — неудобно (потому что лишает код консистентности).
И да, и нет. XMLHttpRequest имеет много разных событий: error, abort, timeout... Это как раз пример того, что ошибок может быть много и разных, и специфика этих ошибок в том, что их нужно по разному обрабатывать. Нас же не смущает, то, что мы используем колбеки для подписки на события. Timeout, abort, error - тоже события. С другой стороны, с "религиозной" точки зрения - "Это другое!".
Нет, это пример того, что бывают события, которые отражают ошибку.
Ну так ошибки и в catch можно по-разному обрабатывать.
Если эти события нужны только для поддержания асинхронии — смущает.
Надо разделять ошибку как результат процесса (тогда ошибок больше возникнуть не может, можно ее вернуть из метода, как в вашем примере, или бросить как в try-catch), или ошибку как событие в процессе (они могут возникнуть еще потом, процесс не остановился, тогда ее нельзя вернуть, не годится ни try-catch, ни ваш подход).
Да, но тут ты на уровне интерфейса (названия событий) понимаешь какие могут быть ошибки.
Возвращаемся к
try...catch
Иначе
Да, можно использовать только
try...catch
, но...В вашем первом примере общая обработка для двух источников ошибки — это преимущество. А если надо разделить обработку — никто не мешает написать два разных блока try…catch
Что же до ReferenceError — да, не вполне удобно, но способ обхода общеизвестен. Всего-то надо объявить переменную блоком выше.
То есть, все таки, лишний код можно писать, но только тогда, когда его ты сам благословил?
В каком смысле "благословил"?
В комментах были возмущения на тему того, что это лишний код, лишние проверки... Хоть ты это не писал, просто у меня накопилось) не наезд, ни в коем случае)
Благословил - я имел ввиду, что когда другие пишут 'лишний' код, это очень плохо, нужно минусовать. Если я пишу - это правильно, я знаю как лучше.
Вот так, грубо говоря.
Повторюсь:
Согласно моему опыту, гораздо чаще приходится обрабатывать ошибки совместно, а не раздельно. Потому и оптимизируется именно совместная обработка ошибок.
Чаще / реже, не значит, что нужно использовать подход обозначенный в статье, так же как и не значит, что не нужно использовать общий подход. Это просто еще один способ (по моему мнению более прогрессивный), не более того. Ну и "субъективно" для меня, читаемость такого кода лучше
Нет. Я понимаю, какие могут быть события. А все ошибки — это все так же
error
. Вы можете сказать, какие ошибки могут там быть?… а что вам мешает использовать больше одного try-catch?
читай мой коммент выше
Не отвечает на мои вопросы.
Разная специфика ошибок, требующая по-разному реагировать.
Кажется, что я столкнулся с 'религиозной' догмой...
На религиозную догму больше похож как раз ваш вариант.
Серьезно? Я говорю, что можно применять разные подходы в зависимости от ситуации. Я не ограничиваюсь лишь своим подходом, меня не пугает ни try...catch, не onError, ни Error-First-Callback. Пока мне не слили рейтинг с кармой я не минусовал людей с другой точкой зрения, а общался. Но в тоже время, меня решили заминусовать. Так где же у меня догма?
Так как раз это прекрасно покрывается разными try-catch и дифференцированной обработкой ошибок в них. В чем проблема?
Как я писал выше, способ описанный в статье, позволяет обрабатывать ошибки по мере их поступления (возможного). Что касается catch, действительно, он ловит все ошибки и мы спокойно их все разгребем. Проблема лишь в том, что мы все это делаем в одной куче.
А я думал, он их обрабатывает после возврата из метода?...
Ну так не делайте в одной куче, делайте столько try-catch, сколько вам надо.
Ну вот мое мнение, как раз в том и состоит, что по отдельности обрабатывать ошибки удобнее (по моему мнению, два
try-catch
в одном блоке уже перебор ) способом из статьи, если нужно все вместе -catch
Но почему?
А еще понимаете ли в чем дело, вызываемый код не знает, как его будут использовать. И как ему возвращать ошибки?..
Естественно, что не знает. Точно также, как и код возвращающий объект типа.
Так и как же вы предлагаете вызываемому коду возвращать ошибки, учитывая, что у вас две разных парадгмы в зависимости от того, как код вызывается?
Кстати, если попытаться отвлечься от войны, можно увидеть, что наш разговор превратился в спор похожий на ситуацию "табуляция против пробела"... =)
И вот я вам по личному опыту могу сказать, что проект в котором в одном файле табы, а в другом — пробелы, намного хуже того, где только табы, или только пробелы (и не важно, какой стиль мне больше нравится).
Как жаль, что я уже не могу поставить вам плюс.
@lair, друг, извини, что я так сильно тебя обидел, и что из-за этого тебе пришлось лепить минусы на все мои сообщения.
Предположу, что лепит не он
В заголовке указан JavaScript, а в примерах TypeScript. Наверное, не стоит подразумевать, что все JavaScript разработчики пишут на последнем. Тем более, что эту статью могут найти через много лет, когда TS может быть уже не таким популярным, и это введёт в заблуждение.
Думаю, что стоит идти в ногу со временем. И стоит заниматься популяризацией TS. Тем более сейчас это происходит настолько органично, что не вызывает ни отторжения.
А через много лет кто-нибудь напишет новую статью.
Вероятно также думали разработчики, которые писали на jQuery в своё время (не смотря на всю разницу между jQuery и TS). Предлагаю все таки придерживаться каких-то правил и указывать TS, если речь идёт о TS, и JS, если речь идёт о чистом JS. Иначе многие начинающие разработчики могут не понять неожиданный синтакс.