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.