Перейти к основному содержимому
Блокчейн28ГЛАВА 28Ошибки: throw, try/catch и результат вместо исключениястек вызововкод выхода 1отката нет

Ошибки: throw, try/catch и результат вместо исключения

Прочитано0%
Квизы0/5
Тренажёрыне пройден0/1
Уровень 1 · Новичок

Зачем это нужно​

В 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 ---
ОТКАТ ПРОТИВ ОСТАНОВКИ: РАЗНЫЕ ПОСЛЕДСТВИЯSolidity: revertвсе записи отмененысостояние как до вызованаружу уходит причинагаз за проделанное списанNode: необработанная ошибка«считаем комиссию» уже напечатанофайлы записаны, запросы отправленыпроцесс завершается кодом 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
первая строка стека: стека нет
ЧТО РЕАЛЬНО ПРИЛЕТАЕТ В CATCHthrow new Error(…)есть messageесть стек с местом броскаthrow "строка"поля message нетстека нетthrow { code: 42 }читать нечегостека нетthrow undefinedчитать нечегостека нетбросить можно что угодно — поэтому пойманное значение имеет тип «неизвестно» и требует проверкипообещать компилятору тип пойманного нельзя: он отвечает отдельной ошибкой TS1196
Только у настоящей ошибки есть текст и стек: у брошенной строки нет ни того, ни другого

Стек невосстановим: если бросили строку, узнать, откуда она прилетела, уже нельзя. Отладка превращается в поиск вслепую.

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);
Отвечено вопросов: 0 из 2
Чем ошибка здесь отличается от revert в Solidity?
Почему бросают Error, а не строку?
Отвечено верно: 0 из 2

Капкан: ошибка в асинхронной функции​

Асинхронность разбирается в следующей главе, но одну её особенность нужно знать уже сейчас, потому что она портит именно обработку ошибок.

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компилятор промолчалпадение обнаружит пользовательфункция возвращает результатвызывающий читает данные без проверкиTS2339: Property 'value' does not existкод возврата компилятора: 2обработать заставили до запускапро забытое исключение компилятор не скажет ничего — про необработанный результат скажет всегдаисключения оставляют для того, что действительно исключительно; ожидаемый отказ возвращают значением
Про забытое исключение компилятор молчит; про необработанный результат — говорит всегда

Вот главный практический довод, измеренный компилятором. Файл, где ошибка бросающей функции нигде не ловится, собирается чисто: ноль замечаний, код возврата 0. Файл, где у результата читают данные без проверки метки, не собирается.

То есть один способ надеется на дисциплину, второй — заставляет.

Как выбирать:

  • ожидаемый отказ (неверный ввод, пустой ответ, нет курса) — возвращают результатом;
  • исключительное (сломанная настройка, недостижимая ветка, программная ошибка) — бросают.

Ровно так же устроен и Solidity: require для внешних данных, assert для того, чего быть не должно.

Отвечено вопросов: 0 из 2
Функция бросает исключение, вызывающий его нигде не ловит. Что скажет компилятор?
Внутри try вызвали асинхронную функцию без await. Куда попадёт ошибка?
Отвечено верно: 0 из 2

Код выхода — то, что видит запускающий​

Успешное завершение — 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, — эксперт читает вывод скрипта, а не только результат.

Модуль В. Тесты, которые требует задание, устроены на том же механизме: проверка «функция должна отказать» ловит исключение и сверяет его текст. Тест, который ждёт отказа и не получает его, обязан упасть — иначе он ничего не проверяет.

Отвечено вопросов: 0 из 2
Почему код выхода скрипта важен для решения на площадке?
Скрипт демонстрации упал на середине. Что помогает продолжить?
Отвечено верно: 0 из 2

Что дальше​

Следующая глава — асинхронность: обещания, ожидание результата и то, почему ошибка внутри асинхронной функции ведёт себя иначе. Здесь это встретилось капканом; там разберём механизм целиком.

Отвечено вопросов: 0 из 1
Что делает возврат значения из finally?
Отвечено верно: 0 из 1

Углубиться​

  • Причина ошибки — как приложить исходную ошибку к своей и увидеть обе трассировки сразу.
  • Глубина стека — сколько кадров среда хранит по умолчанию и почему трассировка иногда обрывается.
  • Обработчики последнего рубежа — перехват необработанных ошибок на уровне процесса: когда это уместно.
  • Ошибки в тестах — как проверить, что функция отказала, и сверить текст отказа.
  • Повтор при сбое — сколько раз и с какой паузой повторять неудачный запрос; про это в главе про сеть.

Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

Куда дальше: проверенные ресурсы​

Видео по теме

Видеоtry catch throw | Обработка ошибок в JS [Курс 2024]Какие-то уроки [Frontend]
ВидеоОбработка ошибок в JavaScript — try catch finally, throw, класс ErrorАлександр Ламков — Friendly Frontend
ВидеоTypeScript: обработка ошибок и асинхронность | Разбор книги Бориса ЧёрногоСергей Лебедев | QA
ВидеоThe PROBLEM With Error Handling in Typescript (and how to fix it)Ethan Niser
ВидеоTry Catch Error Handling With TypeScriptByteGrad

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.