Js глобальная обработка ошибок

Ошибки — это хорошо. Автор материала, перевод которого мы сегодня публикуем, говорит, что уверен в том, что эта идея известна всем. На первый взгляд ошибки кажутся чем-то страшным. Им могут сопутствовать какие-то потери. Ошибка, сделанная на публике, вредит авторитету того, кто её совершил. Но, совершая ошибки, мы на них учимся, а значит, попадая в следующий раз в ситуацию, в которой раньше вели себя неправильно, делаем всё как нужно.

Выше мы говорили об ошибках, которые люди совершают в обычной жизни. Ошибки в программировании — это нечто иное. Сообщения об ошибках помогают нам улучшать код, они позволяют сообщать пользователям наших проектов о том, что что-то пошло не так, и, возможно, рассказывают пользователям о том, как нужно вести себя для того, чтобы ошибок больше не возникало.

Этот материал, посвящённый обработке ошибок в JavaScript, разбит на три части. Сначала мы сделаем общий обзор системы обработки ошибок в JavaScript и поговорим об объектах ошибок. После этого мы поищем ответ на вопрос о том, что делать с ошибками, возникающими в серверном коде (в частности, при использовании связки Node.js + Express.js). Далее — обсудим обработку ошибок в React.js. Фреймворки, которые будут здесь рассматриваться, выбраны по причине их огромной популярности. Однако рассматриваемые здесь принципы работы с ошибками универсальны, поэтому вы, даже если не пользуетесь Express и React, без труда сможете применить то, что узнали, к тем инструментам, с которыми работаете.

Код демонстрационного проекта, используемого в данном материале, можно найти в этом репозитории.

1. Ошибки в JavaScript и универсальные способы работы с ними

Если в вашем коде что-то пошло не так, вы можете воспользоваться следующей конструкцией.

throw new Error('something went wrong')

В ходе выполнения этой команды будет создан экземпляр объекта Error и будет сгенерировано (или, как говорят, «выброшено») исключение с этим объектом. Инструкция throw может генерировать исключения, содержащие произвольные выражения. При этом выполнение скрипта остановится в том случае, если не были предприняты меры по обработке ошибки.

Начинающие JS-программисты обычно не используют инструкцию throw. Они, как правило, сталкиваются с исключениями, выдаваемыми либо средой выполнения языка, либо сторонними библиотеками. Когда это происходит — в консоль попадает нечто вроде ReferenceError: fs is not defined и выполнение программы останавливается.

▍Объект Error

У экземпляров объекта Error есть несколько свойств, которыми мы можем пользоваться. Первое интересующее нас свойство — message. Именно сюда попадает та строка, которую можно передать конструктору ошибки в качестве аргумента. Например, ниже показано создание экземпляра объекта Error и вывод в консоль переданной конструктором строки через обращение к его свойству message.

const myError = new Error('please improve your code')
console.log(myError.message) // please improve your code

Второе свойство объекта, очень важное, представляет собой трассировку стека ошибки. Это — свойство stack. Обратившись к нему можно просмотреть стек вызовов (историю ошибки), который показывает последовательность операций, приведшую к неправильной работе программы. В частности, это позволяет понять — в каком именно файле содержится сбойный код, и увидеть, какая последовательность вызовов функций привела к ошибке. Вот пример того, что можно увидеть, обратившись к свойству stack.

Error: please improve your code
 at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79)
 at Module._compile (internal/modules/cjs/loader.js:689:30)
 at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
 at Module.load (internal/modules/cjs/loader.js:599:32)
 at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
 at Function.Module._load (internal/modules/cjs/loader.js:530:3)
 at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
 at startup (internal/bootstrap/node.js:266:19)
 at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)

Здесь, в верхней части, находится сообщение об ошибке, затем следует указание на тот участок кода, выполнение которого вызвало ошибку, потом описывается то место, откуда был вызван этот сбойный участок. Это продолжается до самого «дальнего» по отношению к ошибке фрагмента кода.

▍Генерирование и обработка ошибок

Создание экземпляра объекта Error, то есть, выполнение команды вида new Error(), ни к каким особым последствиям не приводит. Интересные вещи начинают происходить после применения оператора throw, который генерирует ошибку. Как уже было сказано, если такую ошибку не обработать, выполнение скрипта остановится. При этом нет никакой разницы — был ли оператор throw использован самим программистом, произошла ли ошибка в некоей библиотеке или в среде выполнения языка (в браузере или в Node.js). Поговорим о различных сценариях обработки ошибок.

▍Конструкция try…catch

Блок try...catch представляет собой самый простой способ обработки ошибок, о котором часто забывают. В наши дни, правда, он используется гораздо интенсивнее чем раньше, благодаря тому, что его можно применять для обработки ошибок в конструкциях async/await.

Этот блок можно использовать для обработки любых ошибок, происходящих в синхронном коде. Рассмотрим пример.

const a = 5

try {
    console.log(b) // переменная b не объявлена - возникает ошибка
} catch (err) {
    console.error(err) // в консоль попадает сообщение об ошибке и стек ошибки
}

console.log(a) // выполнение скрипта не останавливается, данная команда выполняется

Если бы в этом примере мы не заключили бы сбойную команду console.log(b) в блок try...catch, то выполнение скрипта было бы остановлено.

▍Блок finally

Иногда случается так, что некий код нужно выполнить независимо от того, произошла ошибка или нет. Для этого можно, в конструкции try...catch, использовать третий, необязательный, блок — finally. Часто его использование эквивалентно некоему коду, который идёт сразу после try...catch, но в некоторых ситуациях он может пригодиться. Вот пример его использования.

const a = 5

try {
    console.log(b) // переменная b не объявлена - возникает ошибка
} catch (err) {
    console.error(err) // в консоль попадает сообщение об ошибке и стек ошибки
} finally {
    console.log(a) // этот код будет выполнен в любом случае
}

▍Асинхронные механизмы — коллбэки

Программируя на JavaScript всегда стоит обращать внимание на участки кода, выполняющиеся асинхронно. Если у вас имеется асинхронная функция и в ней возникает ошибка, скрипт продолжит выполняться. Когда асинхронные механизмы в JS реализуются с использованием коллбэков (кстати, делать так не рекомендуется), соответствующий коллбэк (функция обратного вызова) обычно получает два параметра. Это нечто вроде параметра err, который может содержать ошибку, и result — с результатами выполнения асинхронной операции. Выглядит это примерно так:

myAsyncFunc(someInput, (err, result) => {
    if(err) return console.error(err) // порядок работы с объектом ошибки мы рассмотрим позже
    console.log(result)
})

Если в коллбэк попадает ошибка, она видна там в виде параметра err. В противном случае в этот параметр попадёт значение undefined или null. Если оказалось, что в err что-то есть, важно отреагировать на это, либо так как в нашем примере, воспользовавшись командой return, либо воспользовавшись конструкцией if...else и поместив в блок else команды для работы с результатом выполнения асинхронной операции. Речь идёт о том, чтобы, в том случае, если произошла ошибка, исключить возможность работы с результатом, параметром result, который в таком случае может иметь значение undefined. Работа с таким значением, если предполагается, например, что оно содержит объект, сама может вызвать ошибку. Скажем, это произойдёт при попытке использовать конструкцию result.data или подобную ей.

▍Асинхронные механизмы — промисы

Для выполнения асинхронных операций в JavaScript лучше использовать не коллбэки а промисы. Тут, в дополнение к улучшенной читабельности кода, имеются и более совершенные механизмы обработки ошибок. А именно, возиться с объектом ошибки, который может попасть в функцию обратного вызова, при использовании промисов не нужно. Здесь для этой цели предусмотрен специальный блок catch. Он перехватывает все ошибки, произошедшие в промисах, которые находятся до него, или все ошибки, которые произошли в коде после предыдущего блока catch. Обратите внимание на то, что если в промисе произошла ошибка, для обработки которой нет блока catch, это не остановит выполнение скрипта, но сообщение об ошибке будет не особенно удобочитаемым.

(node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong
(node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

В результате можно порекомендовать всегда, при работе с промисами, использовать блок catch. Взглянем на пример.

Promise.resolve(1)
    .then(res => {
        console.log(res) // 1

        throw new Error('something went wrong')

        return Promise.resolve(2)
    })
    .then(res => {
        console.log(res) // этот блок выполнен не будет
    })
    .catch(err => {
        console.error(err) // о том, что делать с этой ошибкой, поговорим позже
        return Promise.resolve(3)
    })
    .then(res => {
        console.log(res) // 3
    })
    .catch(err => {
        // этот блок тут на тот случай, если в предыдущем блоке возникнет какая-нибудь ошибка
        console.error(err)
    })

▍Асинхронные механизмы и try…catch

После того, как в JavaScript появилась конструкция async/await, мы вернулись к классическому способу обработки ошибок — к try...catch...finally. Обрабатывать ошибки при таком подходе оказывается очень легко и удобно. Рассмотрим пример.

;(async function() {
    try {
        await someFuncThatThrowsAnError()
    } catch (err) {
        console.error(err) // об этом поговорим позже
    }

    console.log('Easy!') // будет выполнено
})()

При таком подходе ошибки в асинхронном коде обрабатываются так же, как в синхронном. В результате теперь, при необходимости, в одном блоке catch можно обрабатывать более широкий диапазон ошибок.

2. Генерирование и обработка ошибок в серверном коде

Теперь, когда у нас есть инструменты для работы с ошибками, посмотрим на то, что мы можем с ними делать в реальных ситуациях. Генерирование и правильная обработка ошибок — это важнейший аспект серверного программирования. Существуют разные подходы к работе с ошибками. Здесь будет продемонстрирован подход с использованием собственного конструктора для экземпляров объекта Error и кодов ошибок, которые удобно передавать во фронтенд или любым механизмам, использующим серверные API. Как структурирован бэкенд конкретного проекта — особого значения не имеет, так как при любом подходе можно использовать одни и те же идеи, касающиеся работы с ошибками.

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

  1. Универсальная обработка ошибок — некий базовый механизм, подходящий для обработки любых ошибок, в ходе работы которого просто выдаётся сообщение наподобие Something went wrong, please try again or contact us, предлагающее пользователю попробовать выполнить операцию, давшую сбой, ещё раз или связаться с владельцем сервера. Эта система не отличается особой интеллектуальностью, но она, по крайней мере, способна сообщить пользователю о том, что что-то пошло не так. Подобное сообщение гораздо лучше, чем «бесконечная загрузка» или нечто подобное.
  2. Обработка конкретных ошибок — механизм, позволяющий сообщить пользователю подробные сведения о причинах неправильного поведения системы и дать ему конкретные советы по борьбе с неполадкой. Например, это может касаться отсутствия неких важных данных в запросе, который пользователь отправляет на сервер, или в том, что в базе данных уже существует некая запись, которую он пытается добавить ещё раз, и так далее.

▍Разработка собственного конструктора объектов ошибок

Здесь мы воспользуемся стандартным классом Error и расширим его. Пользоваться механизмами наследования в JavaScript — дело рискованное, но в данном случае эти механизмы оказываются весьма полезными. Зачем нам наследование? Дело в том, что нам, для того, чтобы код удобно было бы отлаживать, нужны сведения о трассировке стека ошибки. Расширяя стандартный класс Error, мы, без дополнительных усилий, получаем возможности по трассировке стека. Мы добавляем в наш собственный объект ошибки два свойства. Первое — это свойство code, доступ к которому можно будет получить с помощью конструкции вида err.code. Второе — свойство status. В него будет записываться код состояния HTTP, который планируется передавать клиентской части приложения.

Вот как выглядит класс CustomError, код которого оформлен в виде модуля.

class CustomError extends Error {
    constructor(code = 'GENERIC', status = 500, ...params) {
        super(...params)

        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, CustomError)
        }

        this.code = code
        this.status = status
    }
}

module.exports = CustomError

▍Маршрутизация

Теперь, когда наш объект ошибки готов к использованию, нужно настроить структуру маршрутов. Как было сказано выше, нам требуется реализовать унифицированный подход к обработке ошибок, позволяющий одинаково обрабатывать ошибки для всех маршрутов. По умолчанию фреймворк Express.js не вполне поддерживает такую схему работы. Дело в том, что все его маршруты инкапсулированы.

Для того чтобы справиться с этой проблемой, мы можем реализовать собственный обработчик маршрутов и определять логику маршрутов в виде обычных функций. Благодаря такому подходу, если функция маршрута (или любая другая функция) выбрасывает ошибку, она попадёт в обработчик маршрутов, который затем может передать её клиентской части приложения. При возникновении ошибки на сервере мы планируем передавать её во фронтенд в следующем формате, полагая, что для этого будет применяться JSON-API:

{
    error: 'SOME_ERROR_CODE',
    description: 'Something bad happened. Please try again or contact support.'
}

Если на данном этапе происходящие кажется вам непонятным — не беспокойтесь — просто продолжайте читать, пробуйте работать с тем, о чём идёт речь, и постепенно вы во всём разберётесь. На самом деле, если говорить о компьютерном обучении, здесь применяется подход «сверху-вниз», когда сначала обсуждаются общие идеи, а потом осуществляется переход к частностям.

Вот как выглядит код обработчика маршрутов.

const express = require('express')
const router = express.Router()
const CustomError = require('../CustomError')

router.use(async (req, res) => {
    try {
        const route = require(`.${req.path}`)[req.method]

        try {
            const result = route(req) // Передаём запрос функции route
            res.send(result) // Передаём клиенту то, что получено от функции route
        } catch (err) {
            /*
            Сюда мы попадаем в том случае, если в функции route произойдёт ошибка
            */
            if (err instanceof CustomError) {
                /* 
                Если ошибка уже обработана - трансформируем её в 
                возвращаемый объект
                */

                return res.status(err.status).send({
                    error: err.code,
                    description: err.message,
                })
            } else {
                console.error(err) // Для отладочных целей

                // Общая ошибка - вернём универсальный объект ошибки
                return res.status(500).send({
                    error: 'GENERIC',
                    description: 'Something went wrong. Please try again or contact support.',
                })
            }
        }
    } catch (err) {
        /* 
         Сюда мы попадём, если запрос окажется неудачным, то есть,
         либо не будет найдено файла, соответствующего пути, переданному
         в запросе, либо не будет экспортированной функции с заданным
         методом запроса
        */
        res.status(404).send({
            error: 'NOT_FOUND',
            description: 'The resource you tried to access does not exist.',
        })
    }
})

module.exports = router

Полагаем, комментарии в коде достаточно хорошо его поясняют. Надеемся, читать их удобнее, чем объяснения подобного кода, данные после него.

Теперь взглянем на файл маршрутов.

const CustomError = require('../CustomError')

const GET = req => {
    // пример успешного выполнения запроса
    return { name: 'Rio de Janeiro' }
}

const POST = req => {
    // пример ошибки общего характера
    throw new Error('Some unexpected error, may also be thrown by a library or the runtime.')
}

const DELETE = req => {
    // пример ошибки, обрабатываемой особым образом
    throw new CustomError('CITY_NOT_FOUND', 404, 'The city you are trying to delete could not be found.')
}

const PATCH = req => {
    // пример перехвата ошибок и использования CustomError
    try {
        // тут случилось что-то нехорошее
        throw new Error('Some internal error')
    } catch (err) {
        console.error(err) // принимаем решение о том, что нам тут делать

        throw new CustomError(
            'CITY_NOT_EDITABLE',
            400,
            'The city you are trying to edit is not editable.'
        )
    }
}

module.exports = {
    GET,
    POST,
    DELETE,
    PATCH,
}

В этих примерах с самими запросами ничего не делается. Тут просто рассматриваются разные сценарии возникновения ошибок. Итак, например, запрос GET /city попадёт в функцию const GET = req =>..., запрос POST /city попадёт в функцию const POST = req =>... и так далее. Эта схема работает и при использовании параметров запросов. Например — для запроса вида GET /city?startsWith=R. В целом, здесь продемонстрировано, что при обработке ошибок, во фронтенд может попасть либо общая ошибка, содержащая лишь предложение попробовать снова или связаться с владельцем сервера, либо ошибка, сформированная с использованием конструктора CustomError, которая содержит подробные сведения о проблеме.
Данные общей ошибки придут в клиентскую часть приложения в таком виде:

{
    error: 'GENERIC',
    description: 'Something went wrong. Please try again or contact support.'
}

Конструктор CustomError используется так:

throw new CustomError('MY_CODE', 400, 'Error description')

Это даёт следующий JSON-код, передаваемый во фронтенд:

{
    error: 'MY_CODE',
    description: 'Error description'
}

Теперь, когда мы основательно потрудились над серверной частью приложения, в клиентскую часть больше не попадают бесполезные логи ошибок. Вместо этого клиент получает полезные сведения о том, что пошло не так.

Не забудьте о том, что здесь лежит репозиторий с рассматриваемым здесь кодом. Можете его загрузить, поэкспериментировать с ним, и, если надо, адаптировать под нужды вашего проекта.

3. Работа с ошибками на клиенте

Теперь пришла пора описать третью часть нашей системы обработки ошибок, касающуюся фронтенда. Тут нужно будет, во-первых, обрабатывать ошибки, возникающие в клиентской части приложения, а во-вторых, понадобится оповещать пользователя об ошибках, возникающих на сервере. Разберёмся сначала с показом сведений о серверных ошибках. Как уже было сказано, в этом примере будет использована библиотека React.

▍Сохранение сведений об ошибках в состоянии приложения

Как и любые другие данные, ошибки и сообщения об ошибках могут меняться, поэтому их имеет смысл помещать в состояние компонентов. При монтировании компонента данные об ошибке сбрасываются, поэтому, когда пользователь впервые видит страницу, там сообщений об ошибках не будет.

Следующее, с чем надо разобраться, заключается в том, что ошибки одного типа нужно показывать в одном стиле. По аналогии с сервером, здесь можно выделить 3 типа ошибок.

  1. Глобальные ошибки — в эту категорию попадают сообщения об ошибках общего характера, приходящие с сервера, или ошибки, которые, например, возникают в том случае, если пользователь не вошёл в систему и в других подобных ситуациях.
  2. Специфические ошибки, выдаваемые серверной частью приложения — сюда относятся ошибки, сведения о которых приходят с сервера. Например, подобная ошибка возникает, если пользователь попытался войти в систему и отправил на сервер имя и пароль, а сервер сообщил ему о том, что пароль неправильный. Подобные вещи в клиентской части приложения не проверяются, поэтому сообщения о таких ошибках должны приходить с сервера.
  3. Специфические ошибки, выдаваемые клиентской частью приложения. Пример такой ошибки — сообщение о некорректном адресе электронной почты, введённом в соответствующее поле.

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

Здесь будет использоваться встроенная в React система управления состоянием приложения, но, при необходимости, вы можете воспользоваться и специализированными решениями для управления состоянием — такими, как MobX или Redux.

▍Глобальные ошибки

Обычно сообщения о таких ошибках сохраняются в компоненте наиболее высокого уровня, имеющем состояние. Они выводятся в статическом элементе пользовательского интерфейса. Это может быть красное поле в верхней части экрана, модальное окно или что угодно другое. Реализация зависит от конкретного проекта. Вот как выглядит сообщение о такой ошибке.

Сообщение о глобальной ошибке

Теперь взглянем на код, который хранится в файле Application.js.

import React, { Component } from 'react'

import GlobalError from './GlobalError'

class Application extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._resetError = this._resetError.bind(this)
        this._setError = this._setError.bind(this)
    }

    render() {
        return (
            <div className="container">
                <GlobalError error={this.state.error} resetError={this._resetError} />
                <h1>Handling Errors</h1>
            </div>
        )
    }

    _resetError() {
        this.setState({ error: '' })
    }

    _setError(newError) {
        this.setState({ error: newError })
    }
}

export default Application

Как видно, в состоянии, в Application.js, имеется место для хранения данных ошибки. Кроме того, тут предусмотрены методы для сброса этих данных и для их изменения.

Ошибка и метод для сброса ошибки передаётся компоненту GlobalError, который отвечает за вывод сообщения об ошибке на экран и за сброс ошибки после нажатия на значок x в поле, где выводится сообщение. Вот код компонента GlobalError (файл GlobalError.js).

import React, { Component } from 'react'

class GlobalError extends Component {
    render() {
        if (!this.props.error) return null

        return (
            <div
                style={{
                    position: 'fixed',
                    top: 0,
                    left: '50%',
                    transform: 'translateX(-50%)',
                    padding: 10,
                    backgroundColor: '#ffcccc',
                    boxShadow: '0 3px 25px -10px rgba(0,0,0,0.5)',
                    display: 'flex',
                    alignItems: 'center',
                }}
            >
                {this.props.error}
                 
                <i
                    className="material-icons"
                    style={{ cursor: 'pointer' }}
                    onClick={this.props.resetError}
                >
                    close
                </font></i>
            </div>
        )
    }
}

export default GlobalError

Обратите внимание на строку if (!this.props.error) return null. Она указывает на то, что при отсутствии ошибки компонент ничего не выводит. Это предотвращает постоянный показ красного прямоугольника на странице. Конечно, вы, при желании, можете поменять внешний вид и поведение этого компонента. Например, вместо того, чтобы сбрасывать ошибку по нажатию на x, можно задать тайм-аут в пару секунд, по истечении которого состояние ошибки сбрасывается автоматически.

Теперь, когда всё готово для работы с глобальными ошибками, для задания глобальной ошибки достаточно воспользоваться _setError из Application.js. Например, это можно сделать в том случае, если сервер, после обращения к нему, вернул сообщение об общей ошибке (error: 'GENERIC'). Рассмотрим пример (файл GenericErrorReq.js).

import React, { Component } from 'react'
import axios from 'axios'

class GenericErrorReq extends Component {
    constructor(props) {
        super(props)

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Click me to call the backend</button>
            </div>
        )
    }

    _callBackend() {
        axios
            .post('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                }
            })
    }
}

export default GenericErrorReq

На самом деле, на этом наш разговор об обработке ошибок можно было бы и закончить. Даже если в проекте нужно оповещать пользователя о специфических ошибках, никто не мешает просто поменять глобальное состояние, хранящее ошибку и вывести соответствующее сообщение поверх страницы. Однако тут мы не остановимся и поговорим о специфических ошибках. Во-первых, это руководство по обработке ошибок иначе было бы неполным, а во-вторых, с точки зрения UX-специалистов, неправильно будет показывать сообщения обо всех ошибках так, будто все они — глобальные.

▍Обработка специфических ошибок, возникающих при выполнении запросов

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

Сообщение о специфической ошибке

Тут используется тот же принцип, который мы применяли при работе с глобальными ошибками. Только сведения о таких ошибках хранятся в локальном состоянии соответствующих компонентов. Работа с ними очень похожа на работу с глобальными ошибками. Вот код файла SpecificErrorReq.js.

import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest

Тут стоит отметить, что для сброса специфических ошибок недостаточно, например, просто нажать на некую кнопку x. То, что пользователь прочёл сообщение об ошибке и закрыл его, не помогает такую ошибку исправить. Исправить её можно, правильно сформировав запрос к серверу, например — введя в ситуации, показанной на предыдущем рисунке, имя города, который есть в базе. В результате очищать сообщение об ошибке имеет смысл, например, после выполнения нового запроса. Сбросить ошибку можно и в том случае, если пользователь внёс изменения в то, что будет использоваться при формировании нового запроса, то есть — при изменении содержимого поля ввода.

▍Ошибки, возникающие в клиентской части приложения

Как уже было сказано, для хранения данных о таких ошибках можно использовать состояние тех же компонентов, которое используется для хранения данных по специфическим ошибкам, поступающим с сервера. Предположим, мы позволяем пользователю отправить на сервер запрос на удаление города из базы только в том случае, если в соответствующем поле ввода есть какой-то текст. Отсутствие или наличие текста в поле можно проверить средствами клиентской части приложения.

В поле ничего нет, мы сообщаем об этом пользователю

Вот код файла SpecificErrorFrontend.js, реализующий вышеописанный функционал.

import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
            city: '',
        }

        this._callBackend = this._callBackend.bind(this)
        this._changeCity = this._changeCity.bind(this)
    }

    render() {
        return (
            <div>
                <input
                    type="text"
                    value={this.state.city}
                    style={{ marginRight: 15 }}
                    onChange={this._changeCity}
                />
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _changeCity(e) {
        this.setState({
            error: '',
            city: e.target.value,
        })
    }

    _validate() {
        if (!this.state.city.length) throw new Error('Please provide a city name.')
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        try {
            this._validate()
        } catch (err) {
            return this.setState({ error: err.message })
        }

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest

▍Интернационализация сообщений об ошибках с использованием кодов ошибок

Возможно, сейчас вы задаётесь вопросом о том, зачем нам нужны коды ошибок (наподобие GENERIC), если мы показываем пользователю только сообщения об ошибках, полученных с сервера. Дело в том, что, по мере роста и развития приложения, оно, вполне возможно, выйдет на мировой рынок, а это означает, что настанет время, когда создателям приложения нужно будет задуматься о поддержке им нескольких языков. Коды ошибок позволяют отличать их друг от друга и выводить сообщения о них на языке пользователя сайта.

Итоги

Надеемся, теперь у вас сформировалось понимание того, как можно работать с ошибками в веб-приложениях. Нечто вроде console.error(err) следует использовать только в отладочных целях, в продакшн подобные вещи, забытые программистом, проникать не должны. Упрощает решение задачи логирования использование какой-нибудь подходящей библиотеки наподобие loglevel.

Уважаемые читатели! Как вы обрабатываете ошибки в своих проектах?

В этой статье мы познакомимся с инструкцией для обработки ошибок try...catch и throw для генерирования исключений.

Непойманные ошибки

Ошибке в коде могут возникать по разным причинам. Например, вы отправили запрос на сервер, а он дал сбой и прислал ответ, который привёл к неожиданным последствиям. Кроме этой, могут быть тысячи других, а также свои собственные.

Когда возникает ошибка, выполнение кода прекращается, и эта ошибка выводится в консоль:

const json = '{name:"Александр"}';
const person = JSON.parse(json); // Uncaught SyntaxError: Unexpected token n in JSON at position 1
console.log('Это сообщение мы не увидим!');

Непойманная ошибка в JavaScript

Выполнение этого примера остановится при парсинге строки JSON. В консоль будет выведена непойманная ошибка (uncaught error). Она так называется, потому что мы её не поймали (не обработали). Дальше код выполняться не будет и сообщение, которые мы выводим с помощью console.log() не отобразится.

try…catch

Обработка ошибок в JavaScript осуществляется с помощью try...catch.

try...catch – это специальный синтаксис, состоящий из 2 блоков кода:

try {
  // блок кода, в котором имеется вероятность возникновения ошибки
} catch(error) {
  // этот блок выполняется только в случае возникновения ошибки в блоке try
}

Первый блок идёт сразу после ключевого слова try. В этот блок мы помещаем часть кода, в котором есть вероятность возникновения ошибки.

Второй блок располагается за ключевым словом catch. В него помещаем код, который будет выполнен только в том случае, если в первом блоке возникнет ошибка. В круглых скобках после catch указываем параметр error. В этот параметр будет помещена ошибка, которая возникла в блоке try.

Код, приведённый выше мы обернули в try...catch, а именно ту его часть, в котором может возникнуть ошибка:

const text = '{name:"Александр"}';
try {
  const person = JSON.parse(text); // Uncaught SyntaxError: Unexpected token n in JSON at position 1
} catch(error) {
  console.error(error);
  console.log(error.message);
}
console.log('Это сообщение мы увидим!');

Обработка ошибок в JavaScript

Здесь в блоке try произойдет ошибка, так как в данном примере мы специально присвоили переменной text некорректную строку JSON. В catch эта ошибка будет присвоена параметру error, и в нём мы будем просто выводить эту ошибку в консоль с помощью console.error(). Таким образом она будет выведена также красным цветом, но без слова Uncaught, т.к. эта ошибка была поймана.

Ошибка – это объект и у него имеются следующие свойства:

  • message – описание ошибки;
  • name – тип ошибки, например, RangeError при указании значения выходящего за пределы диапазона;
  • stack – строка стека, которая используется в целях отладки; она позволяет узнать о том, что происходило в скрипте на момент возникновения ошибки.

В этом примере мы также написали инструкцию для вывода описание ошибки error.message в консоль с помощью console.log().

Пример функции для проверки корректности JSON:

const isValidJSON = (text) => {
  try {
    JSON.parse(text);
    return true;
  } catch {
    return false;
  }
}

Функция для проверки корректности JSON, написанная с использование инструкции try...catch

При вызове функции, сначала будет выполняться инструкция JSON.parse(text). Если ошибки не возникнет, то возвратится значение true. В противном случае, интерпретатор перейдёт в секцию catch. В итоге будет возвращено false. Кстати здесь catch записан без указания круглых скобок и параметра внутри них. Эта возможность была добавлена в язык, начиная с версии ECMAScript 2019.

Блок «finally»

В JavaScript возможны три формы инструкции try:

  • try...catch
  • try...finally
  • try...catch...finally

Блок finally выполняется всегда, независимо от того возникли ошибки в try или нет. Он выполняется после try, если ошибок не было, и после catch, если ошибки были. Секция finally не имеет параметров.

Пример с использованием finally:

let result = 0;
try {
  result = sum(10, 20);
  console.log('Это сообщение мы не увидим!');
} catch(error) {
  console.log(error.message);
} finally {
  console.log(result);
}

Пример инструкции try с finally в JavaScript

В этом примере произойдет ошибка в секции try, так как sum нигде не определена. После возникновения ошибки интерпретатор перейдём в catch. Здесь с помощью метода console.log() сообщение об ошибке будет выведено в консоль. Затем выполнится инструкция, находящаяся в блоке finally.

В JavaScript имеется также конструкция без catch:

try {
  // ...
} finally {
  // завершаем какие-то действия
}

Инструкция throw

В JavaScript имеется инструкция throw, которая позволяет генерировать ошибку.

Синтаксис инструкции throw:

throw expression;

Как правило, в качестве выражения обычно используют встроенный основной класс для ошибок Error или более конкретный, например: RangeError, ReferenceError, SyntaxError, TypeError, URIError или другой.

Создаём новый объект Error и выбрасываем его в качестве исключения:

throw new Error('Какое-то описание ошибки');

Пример генерирования синтаксической ошибки:

throw new SyntaxError('Описание ошибки');

В качестве выражения можно использовать не только объект ошибки, но и строки, числа, логические значения и другие величины. Но делать это не рекомендуется:

throw 'Значение не является числом';

При обнаружении оператора throw выполнение кода прекращается, и ошибка выбрасывается в консоль.

Например, создадим функцию, которая будет просто выбрасывать новую ошибку:

// создаём стрелочную функцию и присваиваем её переменной myFn
const myFn = () => {
  throw new Error('Описание ошибки');
}
// вызываем функцию
myFn();
console.log('Это сообщение мы не увидим в консоли!');

Генерируем ошибку с помощью throw

Для обработки ошибки обернём вызов функции в try...catch:

const myFn = () => {
  throw new Error('Описание ошибки');
}
try {
  myFn();
} catch(error) {
  console.error(error);
}
console.log('Это сообщение мы увидим в консоли!');

Обрабатываем ошибку в try...catch, сгенерированную с помощью throw

В этом примере вы увидите в консоли ошибку и дальше сообщение, которые мы выводим с помощью console.log(). То есть выполнение кода продолжится.

Кроме встроенных классов ошибок можно создать свои собственные, например, путем расширения Error:

class FormError extends Error {
  constructor(message) {
    super(message);
    this.name = 'FormError';
  }
}

Использование своего класса FormError для отображение ошибок формы:

<form novalidate>
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <button type="submit">Отправить</button>
</form>

<script>
  class FormError extends Error {
    constructor(message) {
      super(message);
      this.name = 'FormError';
    }
  }
  const elForm = document.querySelector('form');
  elForm.onsubmit = (e) => {
    e.preventDefault();
    elForm.querySelectorAll('input').forEach((el) => {
      if (!el.checkValidity()) {
        try {
          throw new FormError(`[name="${el.name}"] ${el.validationMessage}`);
        } catch(error) {
          console.error(`${error.name} ${error.message}`);
        }
      }
    });
  }
</script>

Создание собственного класса ошибок и использование его для отображение ошибок формы в JavaScript

Глобальная ловля ошибок

Возникновение ошибок, которые мы никак не обрабатываем с помощью try, можно очень просто перехватить посредством window.onerror:

window.onerror = function(message, source, lineno, colno, error) {
  // ...
}

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

  • message — строка, содержащее сообщение об ошибке;
  • source — URL-адрес скрипта или документа, в котором произошла ошибка;
  • lineno и colno — соответственно номер строки и столбца, в которой произошла ошибка;
  • error — объект ошибки или null, если соответствующий объект ошибки недоступен;

Передача ошибок на сервер

Что делать с этими ошибками? Их, например, можно передавать на сервер для того чтобы позже можно было проанализировать эти ошибки и принять меры по их устранению.

Пример кода для отправки ошибок, возникающих в браузере на сервер через AJAX с использованием fetch:

window.onerror = (message, source, lineno, colno) => {
  const err = { message, source, lineno, colno };
  fetch('/assets/php/error-log.php', {
    method: 'post',
    body: JSON.stringify(err)
  });
}

На сервере, если, например, сайт на PHP, можно написать такой простенький скрипт:

<?php
define('LOG_FILE', 'logs/' . date('Y-m-d') . '.log');
$json = file_get_contents('php://input');
$data = json_decode($json, true);
try {
  error_log('[' . date('d.m.Y h:i:s') . '] [' . $data['message'] . '] [' . $data['lineno'] . ', ' . $data['colno'] . '] [' . $data['source'] . '] [' . $_SERVER['HTTP_USER_AGENT'] . ']' . PHP_EOL, 3, LOG_FILE);
} catch(Exception $e) {
  $message = implode('; ', $data);
  error_log('[' . date('d.m.Y h:i:s') . '] [' . $message . '] [' . $_SERVER['HTTP_USER_AGENT'] . ']' . PHP_EOL, 3, LOG_FILE);
}

Его следует сохранить в файл /assets/php/error-log.php, а также в этом каталоге создать папку logs для сохранения в ней логов.

В результате когда на клиенте, то есть в браузере будет возникать JavaScript ошибки, они будут сохраняться на сервер в файл следующим образом:

Передача JavaScript ошибок, возникающих в браузере, на сервер

Let me explain how to get stacktraces that are reasonably complete in all browsers.

Error handling in JavaScript

Modern Chrome and Opera fully support the HTML 5 draft spec for ErrorEvent and window.onerror. In both of these browsers you can either use window.onerror, or bind to the ‘error’ event properly:

// Only Chrome & Opera pass the error object.
window.onerror = function (message, file, line, col, error) {
    console.log(message, "from", error.stack);
    // You can send data to your server
    // sendError(data);
};
// Only Chrome & Opera have an error attribute on the event.
window.addEventListener("error", function (e) {
    console.log(e.error.message, "from", e.error.stack);
    // You can send data to your server
    // sendError(data);
})

Unfortunately Firefox, Safari and IE are still around and we have to support them too. As the stacktrace is not available in window.onerror we have to do a little bit more work.

It turns out that the only thing we can do to get stacktraces from errors is to wrap all of our code in a try{ }catch(e){ } block and then look at e.stack. We can make the process somewhat easier with a function called wrap that takes a function and returns a new function with good error handling.

function wrap(func) {
    // Ensure we only wrap the function once.
    if (!func._wrapped) {
        func._wrapped = function () {
            try{
                func.apply(this, arguments);
            } catch(e) {
                console.log(e.message, "from", e.stack);
                // You can send data to your server
                // sendError(data);
                throw e;
            }
        }
    }
    return func._wrapped;
};

This works. Any function that you wrap manually will have good error handling, but it turns out that we can actually do it for you automatically in most cases.

By changing the global definition of addEventListener so that it automatically wraps the callback we can automatically insert try{ }catch(e){ } around most code. This lets existing code continue to work, but adds high-quality exception tracking.

var addEventListener = window.EventTarget.prototype.addEventListener;
window.EventTarget.prototype.addEventListener = function (event, callback, bubble) {
    addEventListener.call(this, event, wrap(callback), bubble);
}

We also need to make sure that removeEventListener keeps working. At the moment it won’t because the argument to addEventListener is changed. Again we only need to fix this on the prototype object:

var removeEventListener = window.EventTarget.prototype.removeEventListener;
window.EventTarget.prototype.removeEventListener = function (event, callback, bubble) {
    removeEventListener.call(this, event, callback._wrapped || callback, bubble);
}

Transmit error data to your backend

You can send error data using image tag as follows

function sendError(data) {
    var img = newImage(),
        src = 'http://yourserver.com/jserror&data=' + encodeURIComponent(JSON.stringify(data));

    img.crossOrigin = 'anonymous';
    img.onload = function success() {
        console.log('success', data);
    };
    img.onerror = img.onabort = function failure() {
        console.error('failure', data);
    };
    img.src = src;
}

Продолжая свою последнюю статью, я хочу поговорить об ошибках. Ошибки — это хорошо — я уверен, вы слышали это раньше. На первый взгляд мы боимся Ошибок, потому что они часто включают в себя обиду или чувство унижения на публике. Выполняя Ошибки, мы фактически узнаем, как не что-то делать и как сделать это лучше в следующий раз.

Очевидно, что речь идет об учении на ошибках (ошибках) в реальной жизни. Ошибки в программировании немного другие. Они предоставляют нам полезные функции, позволяющие улучшить наш код и сообщить пользователю, когда что-то не так (и, возможно, также научить его, как это исправить).

Эта статья состоит из 3 частей, сначала мы рассмотрим ошибки в целом. После этого мы сосредоточимся на бэкэнде (Node.js + Express.js) и, наконец, увидим, как бороться с ошибками в React.js. Я выбрал эти фреймворки, потому что они, безусловно, самые популярные на данный момент, но вы сможете легко применить свои новые знания к другим фреймворкам!

Полный пример проекта доступен на github.

I. Ошибки JavaScript и общая обработка

throw new Error('something went wrong') — создаст экземпляр ошибки в JavaScript и остановит выполнение вашего скрипта, если вы не сделаете что-то с ошибкой. Когда вы начинаете свою карьеру в качестве разработчика JavaScript, вы, скорее всего, не сделаете этого сами, но вместо этого вы видели, как это делают другие библиотеки (или среда выполнения), например ReferenceError: fs is not defined или аналогичный.

Объект ошибки

У объекта Error есть два встроенных свойства, которые мы можем использовать. Первое — это сообщение, которое вы передаете в качестве аргумента конструктору ошибки, например. new Error('This is the message'). Вы можете получить доступ к сообщению через свойство message:

const myError = new Error(‘please improve your code’)
console.log(myError.message) // please improve your code

Второй, очень важный, — это трассировка стека ошибок. Вы можете получить к нему доступ через свойство `stack`. Стек ошибок предоставит вам историю (стек вызовов) того, какие файлы были «ответственны» за возникновение этой ошибки. Стек также включает сообщение вверху, за которым следует фактический стек, начиная с самой последней / изолированной точки ошибки и идя вниз до самого внешнего «ответственного» файла:

Error: please improve your code
 at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79)
 at Module._compile (internal/modules/cjs/loader.js:689:30)
 at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
 at Module.load (internal/modules/cjs/loader.js:599:32)
 at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
 at Function.Module._load (internal/modules/cjs/loader.js:530:3)
 at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
 at startup (internal/bootstrap/node.js:266:19)
 at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)

Выбрасывание и обработка ошибок

Теперь один только экземпляр Error ничего не вызывает. Например. new Error('...') ничего не делает. Когда ошибка становится throw n, становится немного интереснее. Затем, как было сказано ранее, ваш скрипт перестанет выполняться, если вы каким-то образом не обработаете его в своем процессе. Помните, не имеет значения, если вы throw an Ошибка вручную, она вызвана библиотекой или даже самой средой выполнения (узлом или браузером). Давайте посмотрим, как мы можем справиться с этими ошибками в различных сценариях.

try .... catch

Это самый простой, но часто забытый способ обработки ошибок — в наши дни он снова используется гораздо чаще, благодаря async / await, см. Ниже. Это можно использовать для обнаружения любой синхронной ошибки. Пример:

Если мы не заключим console.log(b) в блок try… catch, выполнение скрипта остановится.

… наконец

Иногда необходимо выполнить код в любом случае, независимо от того, есть ошибка или нет. Для этого вы можете использовать третий необязательный блок finally. Часто это то же самое, что просто поставить строку после оператора try… catch, но иногда это может быть полезно.

Асинхронность — обратные вызовы

Асинхронность — одна тема, которую вы всегда должны учитывать при работе с JavaScript. Когда у вас есть асинхронная функция, и внутри этой функции возникает ошибка, ваш скрипт уже продолжит выполнение, поэтому сразу не возникнет какой-либо ошибки. При обработке асинхронных функций с обратными вызовами (кстати, не рекомендуется) вы обычно получаете два параметра в своей функции обратного вызова, которые выглядят примерно так:

Если есть ошибка, параметр err будет равен этой ошибке. Если нет, параметр будет undefined или null. Важно либо вернуть что-то в if(err) -блоке, либо заключить другую инструкцию в else -блок, иначе вы можете получить другую ошибку, например result может быть неопределенным, и вы пытаетесь получить доступ к result.data или аналогичному.

Асинхронность — обещания

Лучший способ справиться с асинхронностью — использовать обещания. Здесь, помимо более удобочитаемого кода, мы также улучшили обработку ошибок. Нам больше не нужно так сильно заботиться о точном обнаружении ошибок, пока у нас есть блок catch. При связывании обещаний блок catch улавливает все ошибки с момента выполнения обещания или последнего блока захвата. Обратите внимание, что обещания без catch-block не завершат сценарий, но предоставят вам менее читаемое сообщение, например

(node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong
(node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

Поэтому всегда добавляйте к своим обещаниям блокировку. Давайте посмотрим:

попробуй … поймай — снова

С введением async / await в JavaScript мы вернулись к исходному способу обработки ошибок, с помощью try… catch… finally, что упрощает их обработку:

Поскольку это тот же способ, который мы использовали для обработки «обычных» синхронных ошибок, при желании проще иметь операторы catch с более широкой областью действия.

II. Создание и обработка ошибок на сервере

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

Мы будем использовать Express.js в качестве среды маршрутизации. Давайте подумаем о структуре, в которой мы хотим иметь наиболее эффективную обработку ошибок. Мы хотим:

  1. Общая обработка ошибок, своего рода запасной вариант, который в основном просто говорит: «Что-то пошло не так, попробуйте еще раз или свяжитесь с нами». Это не особенно умно, но, по крайней мере, уведомляет пользователя о том, что что-то не так — вместо бесконечной загрузки или чего-то подобного.
  2. Специальная обработка ошибок, чтобы предоставить пользователю подробную информацию о том, что не так и как это исправить, например отсутствует какая-то информация, запись уже существует в базе данных и т. д.

Создание настраиваемого конструктора ошибок

Мы воспользуемся существующим конструктором Error и расширим его. Наследование в JavaScript — дело рискованное, но в данном случае я убедился, что это очень полезно. Зачем нам это нужно? Мы по-прежнему хотим, чтобы трассировка стека была удобной для отладки. Расширение встроенного конструктора ошибок JavaScript дает нам бесплатную трассировку стека. Единственное, что мы добавляем, — это code, к которому позже мы можем получить доступ через err.code, а также статус (код статуса http) для передачи во внешний интерфейс.

Как справиться с маршрутизацией

Когда наша настраиваемая ошибка готова к использованию, нам нужно настроить структуру маршрутизации. Как я уже отмечал, нам нужна единая точка истины для обработки ошибок, то есть для каждого маршрута мы хотим иметь одно и то же поведение при обработке ошибок. По умолчанию express не поддерживает это, поскольку все маршруты инкапсулированы.

Чтобы решить эту проблему, мы можем реализовать обработчик маршрута и определить нашу фактическую логику маршрута как обычные функции. Таким образом, в случае, если функция маршрута (или любая функция внутри) выдает ошибку, она будет возвращена обработчику маршрута, который затем может передать ее во внешний интерфейс. Каждый раз, когда в бэкэнде возникает ошибка, мы хотим передать ответ во фронтенд — предполагая JSON API — в следующем формате:

{
    error: 'SOME_ERROR_CODE',
    description: 'Something bad happened. Please try again or     contact support.'
}

Приготовьтесь к поражению. Мои ученики всегда злились на меня, когда я говорил:

Ничего страшного, если ты не все понимаешь с первого взгляда. Просто воспользуйтесь им, и через некоторое время вы поймете, почему это имеет смысл.

Кстати, это также называется нисходящим обучением, и я его большой поклонник.

Так выглядит сам обработчик маршрута:

Надеюсь, вы можете прочитать комментарии в коде, я подумал, что это имеет больше смысла, чем объяснение здесь. Теперь давайте посмотрим, как выглядит настоящий файл маршрута:

В этих примерах я ничего не делаю с фактическим запросом, я просто имитирую различные сценарии ошибок. Так, например, GET /city окажется в строке 3, POST /city окажется в строке 8 и так далее. Это также работает с параметрами запроса, например. GET /city?startsWith=R. По сути, у вас будет либо необработанная ошибка, которую интерфейс получит как

{
    error: 'GENERIC',
    description: 'Something went wrong. Please try again or contact support.'
}

или вы выбросите CustomError вручную, например

throw new CustomError('MY_CODE', 400, 'Error description')

который превращается в

{
    error: 'MY_CODE',
    description: 'Error description'
}

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

Обязательно посмотрите полное репо на github. Не стесняйтесь использовать его для любых своих проектов и модифицировать под свои нужды!

III. Отображение ошибок пользователю

Следующим и последним шагом является управление ошибками во внешнем интерфейсе. Здесь вы хотите обрабатывать ошибки, вызванные самой логикой вашего внешнего интерфейса, с помощью инструментов, описанных в первой части. Однако ошибки из бэкэнда также должны отображаться. Давайте сначала посмотрим, как мы можем отображать ошибки. Как упоминалось ранее, в нашем пошаговом руководстве мы будем использовать React.

Сохранение ошибок в состоянии React

Так же, как и другие данные, ошибки и сообщения об ошибках могут изменяться, поэтому вы хотите поместить их в состояние ваших компонентов. По умолчанию и при подключении вы хотите сбросить ошибки, чтобы при первом просмотре страницы пользователем ошибки не отображались.

Следующее, что нам нужно прояснить, — это разные типы Ошибок с соответствующим визуальным представлением. Как и в бэкэнде, есть 3 типа:

  1. Глобальные ошибки, например одна из наших общих ошибок возвращается из серверной части, или пользователь не вошел в систему и т. д.
  2. Конкретные ошибки, исходящие от серверной части, например пользователь отправляет свои учетные данные для входа в серверную часть. Серверная часть отвечает, что пароль не совпадает. Это не может быть проверено внешним интерфейсом, поэтому оно должно исходить от внутреннего интерфейса.
  3. Конкретные ошибки, вызванные самим интерфейсом, например проверка ввода электронной почты не выполняется.

2. и 3. очень похожи и могут обрабатываться в одном и том же состоянии (при желании), но имеют разное происхождение. Мы увидим в коде, как это разыграется.

Я собираюсь использовать собственную реализацию состояния React, но вы также можете использовать системы управления состоянием, такие как MobX или Redux.

Глобальные ошибки

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

Посмотрим на код:

Как видите, у нас есть ошибка в состоянии Application.js. У нас также есть методы для сброса и изменения значения ошибки. Мы передаем значение и метод сброса компоненту GlobalError, который позаботится об его отображении и сбросе при нажатии на «x». Посмотрим, как выглядит GlobalError-компонент:

Как вы можете видеть в строке 5, мы ничего не визуализируем, если нет ошибки. Это предотвращает постоянное наличие пустого красного поля на нашей странице. Конечно, вы можете изменить внешний вид и поведение этого компонента. Например, вы можете заменить «x» на Timeout, который сбрасывает состояние ошибки через пару секунд.

Теперь вы готовы использовать это глобальное состояние ошибки где угодно, просто передайте _setError из Application.js, и вы можете установить глобальную ошибку, например когда запрос от бэкэнда возвращается с полем error: 'GENERIC'. Пример:

Если вы ленивы, можете остановиться на этом. Даже если у вас есть определенные ошибки, вы всегда можете просто изменить глобальное состояние ошибки и отобразить поле ошибки вверху страницы. Однако я собираюсь показать вам, как обрабатывать и отображать определенные ошибки. Почему? Во-первых, это подробное руководство по обработке ошибок, поэтому я не могу останавливаться на достигнутом. Во-вторых, UX-люди, вероятно, взбесятся, если вы просто отобразите все ошибки глобально.

Обработка конкретных ошибок запроса

Подобно глобальным ошибкам, мы можем иметь локальное состояние ошибки и внутри других компонентов. Порядок действий такой же:

Следует помнить, что очистка ошибки обычно имеет другой триггер. Не имеет смысла использовать символ «x» для удаления ошибки. Здесь было бы разумнее сбросить ошибку при создании нового запроса. Вы также можете сбросить ошибку, когда пользователь вносит изменения, например при изменении входного значения.

Ошибки происхождения веб-интерфейса

Как упоминалось ранее, эти ошибки можно обрабатывать таким же образом (состояние), как и определенные ошибки, исходящие от серверной части. Давайте на этот раз воспользуемся примером с полем ввода и позволим пользователю удалять город только в том случае, если он фактически ввел данные:

Ошибка интернационализации с использованием кода ошибки

Возможно, вам было интересно, почему у нас есть эти коды ошибок, например GENERIC, мы просто отображали описание ошибки, переданное из бэкэнда. Теперь, когда ваше приложение вырастет, вы, надеюсь, завоюете новые рынки и в какой-то момент столкнетесь с проблемой поддержки нескольких языков. Если вы находитесь в этой точке, вы можете использовать упомянутый код ошибки, чтобы отобразить правильный заголовок на языке пользователя.

Надеюсь, вы получили представление о том, как бороться с ошибками. Быстро набираемый и столь же быстро забываемый console.error(err) теперь должен уйти в прошлое. Это важно использовать для отладки, но это не должно попадать в вашу производственную сборку. Чтобы предотвратить это, я бы порекомендовал вам использовать библиотеку протоколирования, я использовал loglevel в прошлом, и мне это очень нравится.

Об авторе: Лукас Гисдер-Дубе стал соучредителем и руководил стартапом в качестве технического директора в течение полутора лет, создавая техническую команду и архитектуру. Покинув стартап, он преподавал программирование в качестве ведущего инструктора в Ironhack, а сейчас создает стартап-агентство и консалтинговое агентство в Берлине. Посетите dube.io, чтобы узнать больше.

Promise rejection handlers are a powerful tool in JavaScript for handling errors that occur in asynchronous code. In this article, we will learn how to build a global promise rejection handler in JavaScript and how it might assist your application manage errors. You can detect unhandled promise rejections using the global unhandledrejection event and take the necessary action, like logging the problem to an error tracking service or showing the user a notice.

A more understandable and simpler syntax for handling promise rejection is provided by error handling with async-await, but it necessitates that you wrap each async function in a try-catch block. Async-await with a global promise rejection handler combined can give your application a strong and reliable error handling system.

What is Promise Rejections?

Promises in JavaScript are a way to handle asynchronous code, such as fetching data from a server or waiting for a user’s input. A promise is an object that represents a value that may not yet be available. Promises have three states −

Pending − The initial state, neither fulfilled nor rejected.

Fulfilled − Meaning that the operation completed successfully.

Rejected − Meaning that the operation failed.

When a promise is rejected, it will trigger a rejection handler if one is attached to the promise. This allows you to handle the error and take appropriate action. However, if there is no rejection handler attached to the promise, the error will go unnoticed and can lead to bugs in your application.

Creating a Global Promise Rejection Handler

JavaScript provides a global unhandledrejection event that can be used to catch unhandled promise rejections. The unhandledrejection event is triggered when a promise is rejected and no rejection handler is attached to the promise.

Example

<html>
<body>
   <div id="error-message"></div>
   <script>
      window.addEventListener("unhandledrejection", event => {
         document.getElementById("error-message").innerHTML = event.reason;
      });
      
      // Create a new promise that intentionally rejects
      const myPromise = new Promise((resolve, reject) => {
         reject(new Error("Intentional error"));
      });

      // Use the promise
      myPromise.catch(error => {
         document.getElementById("error-message").innerHTML = error.message;
      });
   
      // Function to reject the promise when the button is clicked
      function rejectPromise() {
         myPromise.catch(() => {});
      }
   </script>
</body>
</html>

This code attaches an event listener to the global unhandled rejection event, which will be triggered when a promise is rejected and no rejection handler is attached to the promise. The event object passed to the event listener contains a reason property that holds the rejection reason.

The rejected promise can likewise be handled more centrally using this event. For instance, you can use it to display a message to the user, log the problem to an error tracking service, or do other necessary actions. When dealing with large applications, where it might be challenging to keep track of all the promises and associated rejection handlers, this can be extremely helpful.

window.addEventListener("unhandledrejection", event => {
    logErrorToService(event.reason);
    displayErrorMessage(event.reason);
});

Example

We can use the above snippet in our code as follows −

<html>
<body>
   <div id="error-log"></div>
   <div id="error-message"></div> 
   <script>
      window.addEventListener("unhandledrejection", event => {
         logErrorToService(event.reason);
         displayErrorMessage(event.reason);
      });
      function logErrorToService(error) {
         
         // Code to log error to a service
         var errorLog = document.getElementById("error-log");
         errorLog.innerHTML = error;
      }
      function displayErrorMessage(error) {
         
         // Code to display error message on screen
         var errorMessage = document.getElementById("error-message");
         errorMessage.innerHTML = error;
      }
      
      // Create a new promise that intentionally rejects
      const myPromise = new Promise((resolve, reject) => {
         reject(new Error("Intentional error"));
      });
      
      // Use the promise and handle the rejection
      myPromise.catch(error => {
         displayErrorMessage(error.message);
      });
   </script>
</body>
</html>

The unhandledrejection event is caught in this example, and the error is sent to two different functions: one that reports the error to an error tracking service, and another that shows the user an error message.

You should be cautious while implementing this and take care not to interfere with any other error handling techniques that are already in place in your application because this global event listener will catch all unhandled promise rejections on the page.

Error Handling with Async-Await

Async-await is a more readable and cleaner syntax for working with promises. You can handle errors using a standard try-catch block when using async-await.

async function fetchData() {
     try {
         const response = await
        fetch('https://jsonplaceholder.typicode.com/posts/1');
         const data = await response.json();
         console.log(data);
     } catch (error) {
         console.error(error);
     }
}

Example

We can use the above snippet in our code as follows −

<html>
<body>
   <script>
      async function fetchData() {
         try {
            const response = await
            fetch('https://jsonplaceholder.typicode.com/posts/1');
            const data = await response.json();
            var dataDisplay = document.getElementById("data-display");
            dataDisplay.innerHTML = JSON.stringify(data);
         } catch (error) {
            var errorDisplay = document.getElementById("error-display");
            errorDisplay.innerHTML = error;
         }
      }
   </script>
   <button onclick="fetchData()">Fetch Data</button>
   <div id="data-display"></div>
   <div id="error-display"></div>
</body>
</html>

Using the await keyword, the asynchronous function fetchData in this example waits for the promise returned by the fetch method to resolve. The catch block will catch any errors that happen in the try block and log them to the console.

While more accessible and understandable, this approach to error handling necessitates that you wrap each async function in a try-catch block. When working with large systems that have a lot of asynchronous code, this can get laborious and error-prone.

However, combining the use of async-await with a global promise rejection handler can provide a powerful and robust error handling mechanism. By using a global promise rejection handler to catch unhandled promise rejections, you can ensure that all errors in your application are handled, even if you forget to wrap an async function with a try-catch block.

It’s critical to remember that effective error handling is essential for delivering a better user experience and for swiftly fixing faults. A valuable tool that can assist you in doing this is a global promise rejection handler, but it should be used with caution and in conjunction with other error handling systems.

  • Related Articles
  • How to write a global error handler in JavaScript?
  • Handling Promise rejection with a catch while using await in JavaScript
  • How to call promise inside another promise in JavaScript?
  • How to create a Global Variable in Postman?
  • How to delay a loop in JavaScript using async/await with Promise?
  • How to define global variable in a JavaScript function?
  • What is Promise Chaining in JavaScript?
  • How to declare global Variables in JavaScript?
  • How to use Global Variables in JavaScript?
  • How to wrap setTimeout() method in a promise?
  • How to create and use Global Variable in swift
  • How to use finally on promise with then and catch in Javascript?
  • How to add an event handler to an element in JavaScript HTML DOM?
  • How to add an event handler to the specified element in JavaScript?\n
  • How to use handler in android?
Kickstart Your Career

Get certified by completing the course

Get Started

Понравилась статья? Поделить с друзьями:

Интересное по теме:

  • Jungheinrich ошибка 0504
  • Js try catch вызвать ошибку
  • Jrm service container произошла ошибка
  • Jungheinrich ошибка 0331
  • Jquery текст ошибки

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии