Ошибки: throw, try/catch и результат вместо исключения
- Зачем это нужно
- Отказ здесь ничего не откатывает
- throw принимает что угодно — бросать надо Error
- try, catch и finally
- Пойманное значение имеет тип «неизвестно»
- Свой класс ошибки
- Капкан: ошибка в асинхронной функции
- Исключение против результата
- Код выхода — то, что видит запускающий
- Типичные ошибки
- Слова главы
- Тренажёр: набрать вслепую
- На чемпионате
- Что дальше
- Углубиться
- Куда дальше: проверенные ресурсы
Зачем это нужно
В Solidity отказ был операцией с гарантией: revert откатывал транзакцию целиком, и после него состояние выглядело так, будто вызова не было.
Здесь никаких гарантий нет. Ошибка просто останавливает процесс — а всё, что программа успела сделать, остаётся сделанным: напечатанное напечатано, файлы записаны, запросы отправлены. Отменять нечего и некому.
Для скрипта, который проводит демонстрацию, это меняет всё: упасть на середине означает оставить систему в половинчатом состоянии.
Отказ здесь ничего не откатывает
Первая модель бросает ошибку из функции, вызванной из функции:
===== node model1.ts throw =====
считаем комиссию
model1.ts:12
throw new Error('нет курса для пары ' + pair);
^
Error: нет курса для пары USDC/USD1
at loadRate (model1.ts:12:9)
at computeFee (model1.ts:15:10)
at Object.<anonymous> (model1.ts:43:15)
…
--- код выхода: 1 ---
Первая строка вывода — «считаем комиссию» — напечатана до ошибки, и она осталась. Ничего не отменилось.
Второе, что видно: стек вызовов — маршрут, по которому шла программа. Читается сверху вниз: где бросили, кто вызвал, кто вызвал того. Это самый быстрый способ найти место ошибки, и им пользуются постоянно.
Третье: код выхода 1. Он и говорит запускающему инструменту, что скрипт не отработал.
throw принимает что угодно — бросать надо Error
throw new Error("нет курса для пары " + pair);
Язык разрешает бросать что угодно — строку, число, объект, даже пустоту. Делать так нельзя, и вторая модель показывает почему:
throw new Error(...) -> Error, поле message: не число: сто
первая строка стека: at throwError (model2.ts:48:38)
throw "строка" -> строка, поля message нет: не число: сто
первая строка стека: стека нет
throw { code: 42 } -> значение типа object, читать нечего: [object Object]
первая строка стека: стека нет
throw undefined -> значение типа undefined, читать нечего: undefined
первая строка стека: стека нет
Стек невосстановим: если бросили строку, узнать, откуда она прилетела, уже нельзя. Отладка превращается в поиск вслепую.
try, catch и finally
try {
const rate = loadRate(pair);
return amount * rate;
} catch (e) {
console.error("курс не получен:", e instanceof Error ? e.message : e);
return 0;
} finally {
console.log("расчёт завершён");
}
try очерчивает участок, за которым следят. catch получает управление, если внутри что-то бросили. finally выполняется в любом случае — и после обычного выхода, и после ошибки; туда кладут уборку: закрыть соединение, снять блокировку.
Не выполняется finally только тогда, когда процесс не доживает до конца блока — например, при немедленном завершении.
Возврат значения из finally перекрывает и обычный результат, и брошенную ошибку: она просто исчезает. Ошибку, которую съел finally, ищут долго — потому что искать нечего.
Пойманное значение имеет тип «неизвестно»
catch (e) {
console.log(e.message); // ошибка компилятора: 'e' is of type 'unknown'
}
Логика прямая: бросить можно что угодно, значит принимать надо как что угодно. Это тот же unknown из главы про свои типы, и разбирается он так же — проверкой:
catch (e) {
if (e instanceof Error) console.error(e.message);
else console.error("неизвестная ошибка:", e);
}
Пообещать компилятору тип пойманного значения нельзя — на это у него отдельная ошибка:
catch-bad.ts(10,15): error TS18046: 'e' is of type 'unknown'.
catch-bad.ts(15,13): error TS1196: Catch clause variable type annotation must be
'any' or 'unknown' if specified.
Свой класс ошибки
Когда ошибок несколько видов и обрабатывать их надо по-разному, заводят свои классы:
class AmountError extends Error {
constructor(message: string, readonly raw: string) {
super(message);
this.name = "AmountError";
}
}
Три обязательные детали: наследование от встроенного класса, вызов конструктора родителя первой строкой (иначе не будет ни текста, ни стека) и своё имя — без него в журнале будет просто «Error».
Дальше разбор идёт проверкой происхождения — тот же приём, что в Solidity различал ветки по роли:
if (e instanceof AmountError) console.error("плохая сумма:", e.raw);
else if (e instanceof Error) console.error(e.message);
Капкан: ошибка в асинхронной функции
Асинхронность разбирается в следующей главе, но одну её особенность нужно знать уже сейчас, потому что она портит именно обработку ошибок.
try {
fetchHeight(); // без await — ошибка сюда НЕ прилетит
} catch (e) {
// никогда не выполнится
}
Без ожидания результата функция только запускается; ошибка возникнет позже, когда обработчик уже закончил работу. Наружу это выходит как отдельное сообщение о необработанном отказе — и процесс всё равно завершается с кодом 1.
Правило простое: внутри try асинхронный вызов всегда с await.
Исключение против результата
Второй способ сообщить о неудаче — вернуть её значением. Это то самое объединение с меткой из главы про свои типы:
type Parsed =
| { ok: true; value: number }
| { ok: false; error: string };
Третья модель решает одну задачу обоими способами — разбор суммы из строки:
===== вариант с исключением =====
"1000" -> 1000
" 250.5 " -> 250.5
"сто" -> AmountError: не число (raw="сто")
"" -> AmountError: пустая строка (raw="")
"-5" -> AmountError: сумма должна быть больше нуля (raw="-5")
===== вариант с результатом =====
"1000" -> 1000
"сто" -> отказ: не число
"-5" -> отказ: сумма должна быть больше нуля
===== а теперь забудем обработать =====
npx tsc --noEmit --strict forget-throw.ts
(вывода нет)
--- код выхода tsc: 0 ---
npx tsc --noEmit --strict forget-result.ts
forget-result.ts(7,15): error TS2339: Property 'value' does not exist on type 'Parsed'.
Property 'value' does not exist on type '{ ok: false; error: string; }'.
--- код выхода tsc: 2 ---
Вот главный практический довод, измеренный компилятором. Файл, где ошибка бросающей функции нигде не ловится, собирается чисто: ноль замечаний, код возврата 0. Файл, где у результата читают данные без проверки метки, не собирается.
То есть один способ надеется на дисциплину, второй — заставляет.
Как выбирать:
- ожидаемый отказ (неверный ввод, пустой ответ, нет курса) — возвращают результатом;
- исключительное (сломанная настройка, недостижимая ветка, программная ошибка) — бросают.
Ровно так же устроен и Solidity: require для внешних данных, assert для того, чего быть не должно.
Код выхода — то, что видит запускающий
Успешное завершение — 0, необработанная ошибка любого рода — 1. Свой номер задаётся вручную и завершает процесс немедленно, не дожидаясь уборки в finally.
Для скриптов чемпионата это важнее, чем кажется: исполняемый файл запуска решения — это цепочка команд, и каждая следующая обычно запускается, только если предыдущая вернула ноль. Скрипт, который «упал, но вернул 0», сломает всю цепочку молча.
Типичные ошибки
Обращение к тексту пойманного значения без проверки. TS18046: значение имеет тип «неизвестно».
Попытка объявить тип пойманного значения. TS1196: допустимы только «что угодно» и «неизвестно».
Брошена строка вместо ошибки. Ни текста, ни стека — отладка вслепую.
Возврат из finally. Съедает и результат, и ошибку.
Асинхронный вызов внутри try без ожидания. Обработчик не сработает.
Свой класс без вызова конструктора родителя. Нет ни текста, ни стека.
Свой класс без имени. В журнале просто «Error», разбирать нечего.
Перехват всех ошибок подряд с молчаливым продолжением. Программа идёт дальше на неверных данных — хуже, чем падение.
Исключение для ожидаемого отказа. Компилятор не заставит его обработать; лучше вернуть результатом.
Слова главы
- Исключение — прерывание нормального хода программы.
throw— бросить; принимает что угодно, но бросаютError.Error— встроенный класс: имя, текст, стек вызовов, необязательная причина.- Стек вызовов — маршрут, записанный в момент создания ошибки; читается сверху вниз.
try/catch/finally— попробовать, поймать, доделать в любом случае.- Тип пойманного значения — «неизвестно»; разбирают проверкой происхождения.
- Свой класс ошибки — наследование, вызов конструктора родителя, своё имя.
- Результат вместо исключения — объединение с меткой; компилятор заставляет обработать.
- Код выхода — 0 успех, 1 необработанная ошибка, свой номер задаётся вручную.
Тренажёр: набрать вслепую
Цель: скорость ≥ 100 зн/мин, точность ≥ 88%
throw new Error("нет курса для пары " + pair);
На чемпионате
Модуль А. Скрипты развёртывания, раздача стартовых балансов, шаги демонстрации, внешняя служба ликвидации — всё это программы, которые могут упасть на середине.
Практические выводы:
- ожидаемый отказ узла возвращают результатом: компилятор заставит его обработать;
- каждый шаг печатает, что он сделал, — тогда после падения видно, с какого места продолжать;
- код выхода ставят честно: цепочка команд в исполняемом файле запуска опирается именно на него;
- в сообщение ошибки кладут предмет и причину, как в текстах
requireиз Solidity, — эксперт читает вывод скрипта, а не только результат.
Модуль В. Тесты, которые требует задание, устроены на том же механизме: проверка «функция должна отказать» ловит исключение и сверяет его текст. Тест, который ждёт отказа и не получает его, обязан упасть — иначе он ничего не проверяет.
Что дальше
Следующая глава — асинхронность: обещания, ожидание результата и то, почему ошибка внутри асинхронной функции ведёт себя иначе. Здесь это встретилось капканом; там разберём механизм целиком.
Углубиться
- Причина ошибки — как приложить исходную ошибку к своей и увидеть обе трассировки сразу.
- Глубина стека — сколько кадров среда хранит по умолчанию и почему трассировка иногда обрывается.
- Обработчики последнего рубежа — перехват необработанных ошибок на уровне процесса: когда это уместно.
- Ошибки в тестах — как проверить, что функция отказала, и сверить текст отказа.
- Повтор при сбое — сколько раз и с какой паузой повторять неудачный запрос; про это в главе про сеть.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Куда дальше: проверенные ресурсы
- MDN: Error — поля объекта ошибки и свои классы.
- MDN: try…catch — все три блока и их тонкости.
- TypeScript: useUnknownInCatchVariables — почему пойманное значение «неизвестно».
- Node.js: process.exit — коды выхода и когда завершение немедленно.
- Learn JavaScript: обработка ошибок — русскоязычный разбор с задачами.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.