Try-Catch спрятал мои ошибки!
Позвольте мне начать с того, что я проясню одну вещь: JavaScript — отличный язык, и его нельзя винить. Я был полностью виноват — моя ментальная модель обработки ошибок была неполной, и это вызвало проблемы. Отсюда и этот пост.
Но сначала позвольте мне дать вам некоторый контекст. Я писал кучу кода с использованием сторонних API (в частности, повторяющихся API-интерфейсов биллинга и подписки Stripe), а также написал класс-оболочку и несколько серверных обработчиков маршрутов для ответа на запросы от внешнего веб-приложения. Все приложение представляет собой React + TypeScrip + Node с Koa сервером.
В рамках этого я пытался обработать следующие ошибки:
(i) ошибки, выдаваемые API Stripe
(ii) ошибки, выдаваемые моим классом-оболочкой, особенно при извлечении пользовательских данных из базы данных
(iii) ошибки в обработчиках маршрутов, которые возникают из-за комбинации вышеперечисленного.
Во время разработки моими наиболее распространенными ошибками были неполные данные в запросах к серверу и неверные данные, переданные в Stripe.
Чтобы помочь вам визуализировать поток данных, позвольте мне дать вам некоторые сведения о коде на стороне сервера. Обычно так выглядела цепочка вызовов функций:
Route-Handler -> Stripe Wrapper -> Stripe API
Первая вызываемая функция будет в Route-Handler, затем в классе Stripe Wrapper, внутри которого будет вызываться метод Stripe API. Таким образом, в стеке вызовов есть Route-Handler внизу (первая вызываемая функция) и метод Stripe API вверху (последняя вызываемая функция).
Проблема была в том, что я не понял, куда деть свою обработку ошибок. Если бы я не поместил обработчик ошибок в код сервера, то узел рухнул бы (буквально, вышел из выполнения!), а внешний интерфейс получил бы HTTP-ответ с ошибкой (обычно HTTP 5xx err0r). Поэтому я поместил несколько обработчиков try-catch
в различные вызываемые методы и добавил операторы ведения журнала в блок catch
. Таким образом, я мог отладить ошибку, отслеживая журналы.
Пример логики вызова:
function stripeAPI(arg){ console.log('this is the first function') if(!arg) throw new Error('no arg!') // else saveToDb() } function stripeWrapper(){ console.log('this is the second function, about to call the first function') try{ stripeAPI() } catch(err) { // console.log(' this error will not bubble up to the first function that triggered the function calls!') } } function routeHandler(){ console.log('this is the third function, about to call the second function') stripeWrapper() } function callAll(){ try{ routeHandler() return 'done' } catch (err){ console.log('error in callAll():', err) return ' not done ' } } callAll() // I personally use repl.it for all my rapid script executions - check it out.
Проблемы?
1) если я не зарегистрировал ошибку, я потерял ошибку! В приведенном выше фрагменте обратите внимание, что хотя я вызвал first()
без необходимых аргументов, ошибка, определенная в определении first
, не возникла! Кроме того, не определен метод saveToDb()
... и все же это не было поймано! Если вы запустите этот код выше, вы увидите, что он возвращает «готово» — и вы понятия не имеете, что ваша база данных не была обновлена и что-то пошло не так! ☠️☠️☠️
2) На моей консоли было слишком много журналов, повторяющих одну и ту же ошибку. Это также означало, что в продакшене было слишком много логов…🤮
3) код выглядел некрасиво. Почти такой же уродливый, как моя консоль.
4) другие, кто работал с кодом, находили его запутанным. И кошмар отладки. 😒
Ни один из этих результатов не является хорошим, и все они предотвратимы.
Основные понятия
Итак, давайте разберемся с некоторыми основами. Я уверен, что вы их знаете, но некоторые люди могут и не знать, и давайте не будем оставлять их позади!
Немного базовой терминологии:
Ошибка — также известная как «исключение», когда что-то идет не так в коде узла, и программа немедленно завершает работу. Ошибки, если их не обрабатывать, приведут к полной остановке программы, и в консоль будут выбрасываться уродливые сообщения с длинным и, как правило, отвратительным сообщением трассировки стека ошибок.
Throw —оператор throw
— это то, как язык обрабатывает ошибку. Используя throw
, вы генерируете исключение, используя значение, которое вы помещаете после оператора. Обратите внимание, что код после throw
не выполняется — в этом смысле он похож на оператор return
.
Ошибка — существует JavaScript-объект с именем Error
. Ошибка выдается, чтобы помочь программисту понять, что что-то нужно обработать. Думайте об этом как о маленькой бомбе замедленного действия 💣, которая перебрасывается из одной функции в другую внутри цепочки вызовов функций. Технически, вы можете выдать любые данные, включая примитивы JavaScript, как ошибку, но обычно хорошей идеей будет выдавать объект Error
. Обычно вы создаете объект Error
, передавая строку сообщения, например: new Error('This is an error')
. Но простое создание нового объекта Error
💣 бесполезно, так как это только полдела. Вы должны throw
его поймать. Вот так это становится полезным. Языки обычно поставляются со стандартным набором ошибок, но вы можете создать собственное сообщение об ошибке с помощью конструктора new Error('this is my error message')
, и ваше сообщение об ошибке должно помочь вам понять, что происходит. Подробнее об ошибках узла.
Ловить — это то, что вы делаете, когда кто-то бросает в вас что-то, верно? Вы, вероятно, сделали бы это рефлекторно, даже если бы кто-то бросил вам один из этих ... 💣! Оператор catch
в JavaScript позволяет обрабатывать возникающую ошибку 💣. Если вы не поймаете ошибку, то ошибка «пузырится» (или вниз, в зависимости от того, как вы просматриваете стек вызовов), пока не достигнет первой вызванной функции, и там она приведет к сбою программы. В моем примере ошибка, выданная Stripe API, будет всплывать на всем пути к моей функции Route-Handler, если я не поймаю ее где-нибудь по пути и не обработаю ее. Если я не обработаю ошибку, узел выдаст ошибку uncaughtException
, а затем завершит программу.
Вернемся к моему примеру.
Стек вызовов
Route-Handler -> Stripe Wrapper -> Stripe API
Путь ошибки
Stripe API (💣брошено здесь) -› API-оболочка (💣не перехвачено) -› Route-Handler (💣по-прежнему не перехвачено ) -› ccrraashh💥💥💥
Мы хотим избежать сбоев приложений, поскольку это может привести к повреждению ваших данных, несогласованности вашего состояния и тому, что ваш пользователь будет думать, что ваше приложение — отстой. Поэтому вдумчивая обработка ошибок требует многоуровневого анализа.
Есть несколько подробных руководств по обработке ошибок в JavaScript, и одно из моих любимых находится здесь, но здесь я подытожу для вас свои основные знания.
Оператор Try-Catch
Используйте их для корректной обработки ошибок, но будьте осторожны с где и когда. Когда ошибки обнаруживаются и не обрабатываются должным образом, они теряются. Этот процесс «всплывания» происходит только до тех пор, пока ошибка не встретит оператор catch
, и если в цепочке вызовов есть оператор catch
, который перехватывает ошибку, то ошибка не приведет к сбою приложения, но отсутствие обработки ошибки скроет ее! Затем он передается в качестве аргумента в catch
, и поэтому он требует, чтобы вы обработали его там.
try{ // code logic } catch (error) { // handle the error appropriately }
Поэтому очень важно поймать и обработать ошибку в момент, когда она имеет для вас наиболее логичный смысл, когда вам нужно ее отладить. Соблазнительно думать, что вы должны поймать его в самом первом месте, где он появляется (последняя вызванная функция, которая находится прямо на вершине стека вызовов), но это не так!
Route-Handler -> Stripe Wrapper (здесь не поймать!) -> Stripe API
Если я помещу свой try-catch
в Stripe Wrapper, который напрямую вызывает API Stripe, то у меня не будет информации о том, где была вызвана моя функция Stripe Wrapper. Может быть, это был обработчик, может быть, это был другой метод внутри моей обертки, а может быть, он был вообще в другом файле! В этом простом примере он явно вызывается Route-Handler, но в реальном приложении его можно вызывать в нескольких местах.
Вместо этого мне имеет смысл поместить try-catch
в Route-Handler, который является самым первым местом, где начинаются вызовы функций, которые привели к ошибке. Таким образом, вы можете отслеживать стек вызовов (это также называется раскручиванием стека вызовов) и детализировать ошибку. Если я отправлю неверные данные в Stripe, он выдаст ошибку, эта ошибка будет проходить через мой код, пока я ее не поймаю.
Но когда я ее ловлю, мне нужно правильно с ней обращаться, иначе я могу непреднамеренно скрыть эту ошибку. Обработка ошибок обычно означает принятие решения о том, нужно ли мне, чтобы мой внешний пользователь знал, что что-то пошло не так (например, их платеж не работал), или это просто внутренняя ошибка сервера (например, Stripe не может найти идентификатор продукта, который я пройдено), с которым мне нужно изящно справиться, не сбивая с толку моих внешних пользователей и не приводя к сбою кода узла. Если я добавил в базу данных что-то неправильное, то я должен очистить эти ложные записи сейчас. При обработке ошибки рекомендуется записывать ее в журнал, чтобы я мог отслеживать приложение на наличие ошибок и сбоев в производстве и эффективно отлаживать. Так что, по крайней мере, обработка будет включать регистрацию ошибки в операторе catch
. Но...
function stripeAPI(arg){ console.log('this is the first function') if(!arg) throw new Error('no arg!') // else saveToDb() } function stripeWrapper(){ console.log('this is the second function, about to call the first function') try { stripeAPI() } catch(err) { console.log('Oops! err will not bubble up to the first function that triggered the function calls!') } } function routeHandler(){ console.log('this is the third function, about to call the second function') stripeWrapper() } function callAll(){ try { routeHandler() return 'done' } catch (err){ console.log('error in callAll():', err) return ' not done ' } } callAll() // I personally use repl.it for all my rapid script executions - check it out.
… как вы можете видеть выше, если я поймаю его и зарегистрирую на среднем уровне (мой класс Stripe Wrapper), он не достигнет routeHandler
или callAll
, и мое приложение не узнает, что что-то пошло не так. callAll
по-прежнему возвращает done
, и единственное свидетельство того, что что-то пошло не так, было в операторе журнала: 'Oops! err will not bubble up to to first function that triggered the function calls!'
. Если бы мы не добавили туда запись журнала, ошибка исчезла бы бесследно.
Это «скрытие ошибок», и это усложняет отладку. Если я добавлю try-catch
, но ничего не сделаю в операторе catch
, я предотвратю сбой моей программы, но в конечном итоге я «спрячу» проблему! Обычно это приводит к несогласованному состоянию — часть кода моего сервера думает, что все в порядке, и сообщает об этом моему внешнему интерфейсу. Но другая часть кода моего сервера указывала на то, что что-то не так! В этом простом примере легко разобраться, но подумайте о глубоко вложенных вызовах во всем приложении — какой кошмар!
Если вам абсолютно необходимо обработать ошибку в середине стека вызовов, обязательно повторно выдайте ошибку соответствующим образом. Это означает, что оператор catch
должен заканчиваться другой операцией throw error
. Таким образом, ошибка будет выброшена снова и продолжит «пузыриться» к первой функции (нижняя часть стека вызовов), которая инициировала цепочку вызовов, где ее можно будет снова правильно обработать.
Вот как это выглядит, если добавить всего один небольшой повторный бросок в функции stripeWrapper()
. Запустите код и посмотрите на разницу в результате, потому что callAll()
теперь передает ошибку!
function stripeWrapper(){ console.log('this is the second function, about to call the first function') try{ stripeAPI() } catch(err) { console.log('Oops! err will not bubble up to to first function that triggered the function calls!') throw err // add this to re-throw! } } function callAll(){ try{ routeHandler() return 'done' } catch (err){ // catches the re-thrown error and prints it to console! console.log('error in callAll():', err) return ' not done ' } } // I personally use repl.it for all my rapid script executions - check it out.
Поскольку вы выдали ошибку на средней стадии, она перешла на внешнюю границу и там застряла. Код возвращает not done
, и вы можете выяснить, почему ошибка говорит «нет аргумента». Вы также можете увидеть, что он никогда не выполнял saveToDb()
, так как ошибка возникла до того, как этот код мог быть выполнен! Это может быть полезно в тех случаях, когда вы сохраняете данные в базу данных, предполагая, что до этого момента не было ошибок. Представьте себе сохранение в базе данных вещей, которые никогда не должны были быть сохранены — теперь это грязные данные в базе данных! 💩💩💩
Итак, не делайте того, что я делал в первые дни программирования, а просто регистрируйте ошибку на каждом шаге в стеке вызовов и повторно вызывайте ее! это просто означает, что вы получите несколько журналов для каждой ошибки, когда она проходит через стек вызовов! Перехватывайте ошибку только в том месте, где вы можете наиболее эффективно и полезно ее обработать, в идеале один раз в заданной цепочке вызовов.
В общем, очень помогает, если вы помещаете свой оператор try catch
в самую внешнюю (первую вызывающую) функцию, которая находится в нижней части стека вызовов. Вы можете определить это как место, где ошибка всплывает непосредственно перед тем, как выдать ошибку uncaughtException
. Это хорошее место, чтобы поймать его, зарегистрировать и обработать.
Чтобы увидеть разницу в обработке, когда вы не используете try-catch
, просто измените callAll()
, чтобы он выглядел следующим образом:
function callAll(){ routeHandler() // this won't run! console.log('This function is not contained inside a try-catch, so will crash the node program.') } callAll() // I personally use repl.it for all my rapid script executions - check it out.
Вы заметите, что оператор console.log
здесь никогда не запускается, потому что программа аварийно завершает работу, когда routeHandler()
завершает выполнение.
Эмпирические правила 👍👍👍
Итак, давайте резюмируем несколько простых правил, которые покроют более 90% ваших потребностей:
i) не засоряйте свой код операторами try-catch
ii) по возможности catch
только один раз в заданной цепочке вызовов функций. Если вы можете вообще избежать ловли и просто полагаться на процесс выхода и выявления проблем, отлично. Перехватывайте только в том случае, если вам нужно каким-то образом обработать данные или не нарушать работу пользователя.
iii) попытайтесь разместить этот catch
на самой внешней границе - первая функция, которая запускает цепочку вызовов функций (нижняя часть стека вызовов)
iv) не оставляйте оператор catch
пустым, чтобы предотвратить сбой вашей программы! Если вы не справитесь с этим, скорее всего, это приведет к несогласованному состоянию между вашим интерфейсом и сервером, а это может быть опасно и привести к ужасному пользовательскому опыту. Это настоящая 💣!
v) не используйте оператор catch
только в середине стека вызовов, а не на внешней границе. Это приведет к тому, что ошибка будет «скрыта» в середине вашего кода, где она не поможет вам правильно отлаживать или управлять данными. Другие, кто работает с вашим кодом, узнают, где вы живете, и отключат ваше интернет-соединение.
vi) ловите его там, где вам нужно это знать, и где вы можете осмысленно сделать все необходимое, чтобы навести порядок.
Stripe API (💣бросили сюда) -› API-оболочка (💣прохождение) -› Route-Handler (💣поймал, обработал , зарегистрирован) -›😺😺😺
Не стесняйтесь улучшать, исправлять или делиться со мной в Twitter: @ZubinPratap
Постскриптум
Я искренне верю, что ваши самые ценные ресурсы — это ваше время, усилия и деньги. Из них самым важным ресурсом является время, потому что два других могут быть возобновлены и восстановлены. Поэтому, если вы собираетесь потратить время на что-то, убедитесь, что это приближает вас к этой цели.
Имея это в виду, если вы хотите потратить 3 часа со мной, чтобы найти кратчайший путь к обучению кодированию (особенно если вы меняете профессию, как я…), тогда перейдите на мой сайт курса и используйте форму там зарегистрируйтесь (не всплывающее окно!). Если вы добавите в сообщение слова СРЕДНИЙ ЧИТАТЕЛЬ, я узнаю, что вы являетесь читателем Medium, и отправлю вам промокод, потому что, как и вы, интернет дал мне хороший старт.
Кроме того, если вы хотите узнать больше о том, как я изменил свою карьеру, посмотрите выпуск 53 подкаста freeCodeCamp, где Куинси (основатель FreeCodeCamp) и я делимся своим опытом смены карьеры которые могут помочь вам в вашем путешествии. Вы также можете получить доступ к подкасту в iTunes, Stitcher и Spotify.