Меню

Обработка ошибок на стороне клиента

This XML file does not appear to have any style information associated with it. The document tree is shown below.

<wsdl:definitions xmlns:xsd=«http://www.w3.org/2001/XMLSchema» xmlns:wsdl=«http://schemas.xmlsoap.org/wsdl/» xmlns:tns=«http://soap.ws.javastudy.ru/» xmlns:soap=«http://schemas.xmlsoap.org/wsdl/soap/» xmlns:ns1=«http://schemas.xmlsoap.org/soap/http» name=«HelloSoap» targetNamespace=«http://soap.ws.javastudy.ru/»>

<wsdl:types>

<xs:schema xmlns:xs=«http://www.w3.org/2001/XMLSchema» xmlns:tns=«http://soap.ws.javastudy.ru/» attributeFormDefault=«unqualified» elementFormDefault=«unqualified» targetNamespace=«http://soap.ws.javastudy.ru/»>

<xs:element name=«exceptionTest» type=«tns:exceptionTest»/>

<xs:element name=«exceptionTestResponse» type=«tns:exceptionTestResponse»/>

<xs:element name=«getGoods» type=«tns:getGoods»/>

<xs:element name=«getGoodsResponse» type=«tns:getGoodsResponse»/>

<xs:element name=«goods» type=«tns:goods»/>

<xs:element name=«sayHelloTo» type=«tns:sayHelloTo»/>

<xs:element name=«sayHelloToResponse» type=«tns:sayHelloToResponse»/>

<xs:element name=«testService» type=«tns:testService»/>

<xs:element name=«testServiceResponse» type=«tns:testServiceResponse»/>

<xs:complexType name=«testService»>

<xs:sequence/>

</xs:complexType>

<xs:complexType name=«testServiceResponse»>

<xs:sequence>

<xs:element minOccurs=«0» name=«return» type=«xs:string»/>

</xs:sequence>

</xs:complexType>

<xs:complexType name=«getGoods»>

<xs:sequence/>

</xs:complexType>

<xs:complexType name=«getGoodsResponse»>

<xs:sequence>

<xs:element minOccurs=«0» name=«return» type=«tns:goods»/>

</xs:sequence>

</xs:complexType>

<xs:complexType name=«goods»>

<xs:sequence>

<xs:element name=«id» type=«xs:int»/>

<xs:element minOccurs=«0» name=«name» type=«xs:string»/>

</xs:sequence>

</xs:complexType>

<xs:complexType name=«sayHelloTo»>

<xs:sequence>

<xs:element minOccurs=«0» name=«text» type=«xs:string»/>

</xs:sequence>

</xs:complexType>

<xs:complexType name=«sayHelloToResponse»>

<xs:sequence>

<xs:element minOccurs=«0» name=«return» type=«xs:string»/>

</xs:sequence>

</xs:complexType>

<xs:complexType name=«exceptionTest»>

<xs:sequence>

<xs:element minOccurs=«0» name=«text» type=«xs:string»/>

</xs:sequence>

</xs:complexType>

<xs:complexType name=«exceptionTestResponse»>

<xs:sequence/>

</xs:complexType>

<xs:complexType name=«exceptionTrace»>

<xs:sequence>

<xs:element minOccurs=«0» name=«trace» type=«xs:string»/>

</xs:sequence>

</xs:complexType>

<xs:element name=«MyWebserviceException» type=«tns:MyWebserviceException»/>

<xs:complexType name=«MyWebserviceException»>

<xs:sequence>

<xs:element minOccurs=«0» name=«exceptionTrace» type=«tns:exceptionTrace»/>

<xs:element minOccurs=«0» name=«message» type=«xs:string»/>

</xs:sequence>

</xs:complexType>

</xs:schema>

</wsdl:types>

<wsdl:message name=«testService»>

<wsdl:part element=«tns:testService» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:message name=«exceptionTestResponse»>

<wsdl:part element=«tns:exceptionTestResponse» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:message name=«getGoods»>

<wsdl:part element=«tns:getGoods» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:message name=«testServiceResponse»>

<wsdl:part element=«tns:testServiceResponse» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:message name=«sayHelloTo»>

<wsdl:part element=«tns:sayHelloTo» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:message name=«exceptionTest»>

<wsdl:part element=«tns:exceptionTest» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:message name=«getGoodsResponse»>

<wsdl:part element=«tns:getGoodsResponse» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:message name=«MyWebserviceException»>

<wsdl:part element=«tns:MyWebserviceException» name=«MyWebserviceException»></wsdl:part>

</wsdl:message>

<wsdl:message name=«sayHelloToResponse»>

<wsdl:part element=«tns:sayHelloToResponse» name=«parameters»></wsdl:part>

</wsdl:message>

<wsdl:portType name=«WebserviceSEI»>

<wsdl:operation name=«testService»>

<wsdl:input message=«tns:testService» name=«testService»></wsdl:input>

<wsdl:output message=«tns:testServiceResponse» name=«testServiceResponse»></wsdl:output>

</wsdl:operation>

<wsdl:operation name=«getGoods»>

<wsdl:input message=«tns:getGoods» name=«getGoods»></wsdl:input>

<wsdl:output message=«tns:getGoodsResponse» name=«getGoodsResponse»></wsdl:output>

</wsdl:operation>

<wsdl:operation name=«sayHelloTo»>

<wsdl:input message=«tns:sayHelloTo» name=«sayHelloTo»></wsdl:input>

<wsdl:output message=«tns:sayHelloToResponse» name=«sayHelloToResponse»></wsdl:output>

</wsdl:operation>

<wsdl:operation name=«exceptionTest»>

<wsdl:input message=«tns:exceptionTest» name=«exceptionTest»></wsdl:input>

<wsdl:output message=«tns:exceptionTestResponse» name=«exceptionTestResponse»></wsdl:output>

<wsdl:fault message=«tns:MyWebserviceException» name=«MyWebserviceException»></wsdl:fault>

</wsdl:operation>

</wsdl:portType>

<wsdl:binding name=«HelloSoapSoapBinding» type=«tns:WebserviceSEI»>

<soap:binding style=«document» transport=«http://schemas.xmlsoap.org/soap/http»/>

<wsdl:operation name=«testService»>

<soap:operation soapAction=«» style=«document»/>

<wsdl:input name=«testService»>

<soap:body use=«literal»/>

</wsdl:input>

<wsdl:output name=«testServiceResponse»>

<soap:body use=«literal»/>

</wsdl:output>

</wsdl:operation>

<wsdl:operation name=«getGoods»>

<soap:operation soapAction=«» style=«document»/>

<wsdl:input name=«getGoods»>

<soap:body use=«literal»/>

</wsdl:input>

<wsdl:output name=«getGoodsResponse»>

<soap:body use=«literal»/>

</wsdl:output>

</wsdl:operation>

<wsdl:operation name=«sayHelloTo»>

<soap:operation soapAction=«» style=«document»/>

<wsdl:input name=«sayHelloTo»>

<soap:body use=«literal»/>

</wsdl:input>

<wsdl:output name=«sayHelloToResponse»>

<soap:body use=«literal»/>

</wsdl:output>

</wsdl:operation>

<wsdl:operation name=«exceptionTest»>

<soap:operation soapAction=«» style=«document»/>

<wsdl:input name=«exceptionTest»>

<soap:body use=«literal»/>

</wsdl:input>

<wsdl:output name=«exceptionTestResponse»>

<soap:body use=«literal»/>

</wsdl:output>

<wsdl:fault name=«MyWebserviceException»>

<soap:fault name=«MyWebserviceException» use=«literal»/>

</wsdl:fault>

</wsdl:operation>

</wsdl:binding>

<wsdl:service name=«HelloSoap»>

<wsdl:port binding=«tns:HelloSoapSoapBinding» name=«HelloSoapPort»>

<soap:address location=«http://localhost:8080/soap/webserviceSEI»/>

</wsdl:port>

</wsdl:service>

</wsdl:definitions>

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

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

Если вам нужно обработать синхронную ошибку, то вы можете, для начала, с помощью инструкции throw, выдать такую ошибку в обработчике запроса Express. Обратите внимание на то, что обработчики запросов ещё называют «контроллерами», но я предпочитаю использовать термин «обработчик запросов» так как он кажется мне понятнее.

Вот как это выглядит:

app.post('/testing', (req, res) => {
  throw new Error('Something broke! ')
})

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

Вот что делает стандартный обработчик ошибок Express:

  1. Устанавливает код состояния HTTP-ответа в значение 500.
  2. Отправляет сущности, выполнившей запрос, текстовый ответ.
  3. Логирует текстовый ответ в консоль.

Сообщение об ошибке, выведенное в консоль

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

Для обработки асинхронных ошибок нужно отправить ошибку обработчику ошибок Express через аргумент next:

app.post('/testing', async (req, res, next) => {
  return next(new Error('Something broke again! '))
})

Вот что попадёт в консоль при логировании этой ошибки.

Сообщение об ошибке, выведенное в консоль

Если вы пользуетесь в Express-приложении конструкцией async/await, то вам понадобится использовать функцию-обёртку, наподобие express-async-handler. Это позволяет писать асинхронный код без блоков try/catch. Подробнее об async/await в Express можно почитать здесь.

const asyncHandler = require('express-async-handler')

app.post('/testing', asyncHandler(async (req, res, next) => {
  // Сделать что-нибудь
}))

После того, как обработчик запроса обёрнут в express-async-handler, то можно, так же, как было описано выше, выбросить ошибку с использованием инструкции throw. Эта ошибка попадёт к обработчику ошибок Express.

app.post('/testing', asyncHandler(async (req, res, next) => {
  throw new Error('Something broke yet again! ')
}))

Сообщение об ошибке, выведенное в консоль

Написание собственного обработчика ошибок

Обработчики ошибок Express принимают 4 аргумента:

  1. error
  2. req
  3. res
  4. next

Размещать их нужно после промежуточных обработчиков и маршрутов.

app.use(/*...*/)
app.get(/*...*/)
app.post(/*...*/)
app.put(/*...*/)
app.delete(/*...*/)

// Собственный обработчик ошибок нужно поместить после всех остальных промежуточных обработчиков
app.use((error, req, res, next) => { /* ... */ })

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

  1. Сформировать и отправить подходящий код состояния ответа.
  2. Сформировать и отправить подходящий ответ.

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

  1. Ошибка 400 Bad Request. Используется в двух ситуациях. Во-первых — тогда, когда пользователь не включил в запрос необходимое поле (например — в отправленной платёжной форме не заполнено поле со сведениями о кредитной карте). Во-вторых — тогда, когда в запросе содержатся некорректные данные (например — ввод в поле пароля и в поле подтверждения пароля разных паролей).
  2. Ошибка 401 Unauthorized. Этот код состояния ответа применяется в том случае, если пользователь ввёл неправильные учётные данные (вроде имени пользователя, адреса электронной почты или пароля).
  3. Ошибка 403 Forbidden. Используется в тех случаях, когда пользователю не разрешён доступ к конечной точке.
  4. Ошибка 404 Not Found. Применяется в тех случаях, когда конечную точку невозможно обнаружить.
  5. Ошибка 500 Internal Server Error. Применяется тогда, когда запрос, отправленный фронтендом, сформирован правильно, но на бэкенде при этом возникла какая-то ошибка.

После того, как определён подходящий код состояния ответа, его нужно установить с помощью res.status:

app.use((error, req, res, next) => {
  // Ошибка, выдаваемая в ответ на неправильно сформированный запрос
  res.status(400)
  res.json(/* ... */)
})

Код состояния ответа должен соответствовать сообщению об ошибке. Для этого нужно отправлять код состояния вместе с ошибкой.

Легче всего это сделать с помощью пакета http-errors. Он позволяет отправлять в ошибке три фрагмента информации:

  1. Код состояния ответа.
  2. Сообщение, сопутствующее ошибке.
  3. Любые данные, которые нужно отправить (это необязательно).

Вот как установить пакет http-errors:

npm install http-errors --save

Вот как этим пакетом пользоваться:

const createError = require('http-errors')

// Создание ошибки
throw createError(status, message, properties)

Рассмотрим пример, который позволит как следует в этом всём разобраться.

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

Вот что нам нужно будет сделать при создании ошибки:

  1. Установить код состояния ответа как 400 Bad Request (ведь пользователь ввёл неправильные данные). Это будет наш первый параметр.
  2. Отправить вызывающей стороне сообщение наподобие User not found. Это будет второй параметр.

app.put('/testing', asyncHandler(async (req, res) => {
  const { email } = req.body
  const user = await User.findOne({ email })

  // Если пользователь не найден - выбросим ошибку
  if (!user) throw createError(400, `User '${email}' not found`)
}))

Получить код состояния можно с помощью конструкции error.status, а сообщение ошибки — с помощью error.message:

// Логирование ошибки
app.use((error, req, res, next) => {
  console.log('Error status: ', error.status)
  console.log('Message: ', error.message)
})

Результат логирования ошибки в консоли

Затем состояние ответа устанавливают с помощью res.status, а сообщение записывают в res.json:

app.use((error, req, res, next) => {
  // Установка кода состояния ответа
  res.status(error.status)

  // Отправка ответа
  res.json({ message: error.message })
})

Лично я предпочитаю отправлять в подобных ответах код состояния, сообщение и результат трассировки стека. Это облегчает отладку.

app.use((error, req, res, next) => {
  // Установка кода состояния ответа
  res.status(error.status)

  // Отправка ответа
  res.json({
    status: error.status,
    message: error.message,
    stack: error.stack
  })
})

▍Код состояния ответа, используемый по умолчанию

Если источником ошибки не является createError, то у неё не будет свойства status. Вот пример, в котором сделана попытка прочесть несуществующий файл с помощью fs.readFile:

const fs = require('fs')
const util = require('util')

// Преобразуем readFile из функции, использующей коллбэки, в async/await-функцию.
// Подробности об этом смотрите здесь: https://zellwk.com/blog/callbacks-to-promises
const readFilePromise = util.promisify(fs.readFile)

app.get('/testing', asyncHandler(async (req, res, next) => {
  const data = await readFilePromise('some-file')
})

У такого объекта ошибки не будет свойства status:

app.use((error, req, res, next) => {
  console.log('Error status: ', error.status)
  console.log('Message: ', error.message)
})

Результат логирования ошибки в консоли

В подобных случаях можно задать код ошибки, используемый по умолчанию. А именно, речь идёт об ошибке 500 Internal Server Error:

app.use((error, req, res, next) => {
  res.status(error.status || 500)
  res.json({
    status: error.status,
    message: error.message,
    stack: error.stack
  })
})

▍Изменение кода состояния ошибки

Предположим, мы собираемся прочитать некий файл, воспользовавшись данными, предоставленными пользователем. Если такого файла не существует, это значит, что нам нужно выдать ошибку 400 Bad Request. Ведь в том, что файл найти не удаётся, нет вины сервера.

В подобном случае нужно воспользоваться конструкцией try/catch для перехвата исходной ошибки. Затем нужно воссоздать объект ошибки с помощью createError:

app.get('/testing', asyncHandler(async (req, res, next) => {
  try {
    const { file } = req.body
    const contents = await readFilePromise(path.join(__dirname, file))
  } catch (error) {
    throw createError(400, `File ${file} does not exist`)
  }
})

▍Обработка ошибок 404

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

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

// Промежуточные обработчики...
// Маршруты...

app.use((req, res, next) => {
  next(createError(404))
})

// Обработчик ошибок...

Сведения об ошибке

▍Замечания об ошибке ERR_HTTP_HEADERS_SENT

Не впадайте в панику если видите сообщение об ошибке ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client. Она возникает из-за того, что в одном и том же обработчике многократно вызывается метод, устанавливающий заголовки ответа. Вот методы, вызов которых приводит к автоматической установке заголовков ответа:

  1. res.send
  2. res.json
  3. res.render
  4. res.sendFile
  5. res.sendStatus
  6. res.end
  7. res.redirect

Так, например, если вы вызовете методы res.render и res.json в одном и том же обработчике ответа, то вы получите ошибку ERR_HTTP_HEADERS_SENT:

app.get('/testing', (req, res) => {
  res.render('new-page')
  res.json({ message: '¯_(ツ)_/¯' })
})

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

▍Обработка ошибок и потоковая передача данных

Если что-то идёт не так при потоковой передаче ответа фронтенду, то можно столкнуться с той же самой ошибкой ERR_HTTP_HEADERS_SENT.

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

app.use((error, req, res, next) => {
  // Сделать это нужно только в том случае, если ответ передаётся в потоковом режиме
  if (res.headersSent) {
    return next(error)
  }

  // Остальной код обработки ошибок
})

Итоги

Сегодня я рассказал вам всё, что знаю об обработке ошибок в Express. Надеюсь, это поможет вам писать более надёжные Express-приложения.

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

Привет! Представляю вашему вниманию перевод статьи «Error and Transaction Handling in SQL Server. Part One – Jumpstart Error Handling» автора Erland Sommarskog.

1. Введение

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

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

Все статьи описывают обработку ошибок и транзакций в SQL Server для версии 2005 и более поздних версий.

1.1 Зачем нужна обработка ошибок?

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

Мы часто хотим, чтобы в базе данных изменения были атомарными. Например, задача по переводу денег с одного счета на другой. С этой целью мы должны изменить две записи в таблице CashHoldings и добавить две записи в таблицу Transactions. Абсолютно недопустимо, чтобы ошибки или сбой привели к тому, что деньги будут переведены на счет получателя, а со счета отправителя они не будут списаны. По этой причине обработка ошибок также касается и обработки транзакций. В приведенном примере нам нужно обернуть операцию в BEGIN TRANSACTION и COMMIT TRANSACTION, но не только это: в случае ошибки мы должны убедиться, что транзакция откачена.

2. Основные команды

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

2.1 TRY-CATCH

Основным механизмом обработки ошибок является конструкция TRY-CATCH, очень напоминающая подобные конструкции в других языках. Структура такова:

BEGIN TRY
   <обычный код>
END TRY
BEGIN CATCH
   <обработка ошибок>
END CATCH

Если какая-либо ошибка появится в <обычный код>, выполнение будет переведено в блок CATCH, и будет выполнен код обработки ошибок.

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

Вот очень быстрый пример:

BEGIN TRY
   DECLARE @x int
   SELECT @x = 1/0
   PRINT 'Not reached'
END TRY
BEGIN CATCH 
   PRINT 'This is the error: ' + error_message()
END CATCH

Результат выполнения: This is the error: Divide by zero error encountered.

Мы вернемся к функции error_message() позднее. Стоит отметить, что использование PRINT в обработчике CATCH приводится только в рамках экспериментов и не следует делать так в коде реального приложения.

Если <обычный код> вызывает хранимую процедуру или запускает триггеры, то любая ошибка, которая в них возникнет, передаст выполнение в блок CATCH. Если более точно, то, когда возникает ошибка, SQL Server раскручивает стек до тех пор, пока не найдёт обработчик CATCH. И если такого обработчика нет, SQL Server отправляет сообщение об ошибке напрямую клиенту.

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

CREATE PROCEDURE inner_sp AS
   BEGIN TRY
      PRINT 'This prints'
      SELECT * FROM NoSuchTable
      PRINT 'This does not print'
   END TRY
   BEGIN CATCH
      PRINT 'And nor does this print'
   END CATCH
go
EXEC inner_sp

Выходные данные:

This prints
Msg 208, Level 16, State 1, Procedure inner_sp, Line 4
Invalid object name 'NoSuchTable'

Как можно видеть, блок TRY присутствует, но при возникновении ошибки выполнение не передается блоку CATCH, как это ожидалось. Это применимо ко всем ошибкам компиляции, таким как пропуск колонок, некорректные псевдонимы и тому подобное, которые возникают во время выполнения. (Ошибки компиляции могут возникнуть в SQL Server во время выполнения из-за отложенного разрешения имен – особенность, благодаря которой SQL Server позволяет создать процедуру, которая обращается к несуществующим таблицам.)

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

CREATE PROCEDURE outer_sp AS
   BEGIN TRY
      EXEC inner_sp
   END TRY
   BEGIN CATCH
      PRINT 'The error message is: ' + error_message()
   END CATCH
go
EXEC outer_sp

Теперь мы получим на выходе это:

This prints
The error message is: Invalid object name 'NoSuchTable'.

На этот раз ошибка была перехвачена, потому что сработал внешний обработчик CATCH.

2.2 SET XACT_ABORT ON

В начало ваших хранимых процедур следует всегда добавлять это выражение:

SET XACT_ABORT, NOCOUNT ON

Оно активирует два параметра сессии, которые выключены по умолчанию в целях совместимости с предыдущими версиями, но опыт доказывает, что лучший подход – это иметь эти параметры всегда включенными. Поведение SQL Server по умолчанию в той ситуации, когда не используется TRY-CATCH, заключается в том, что некоторые ошибки прерывают выполнение и откатывают любые открытые транзакции, в то время как с другими ошибками выполнение последующих инструкций продолжается. Когда вы включаете XACT_ABORT ON, почти все ошибки начинают вызывать одинаковый эффект: любая открытая транзакция откатывается, и выполнение кода прерывается. Есть несколько исключений, среди которых наиболее заметным является выражение RAISERROR.

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

Для надежной обработки ошибок в SQL Server вам необходимы как TRY-CATCH, так и SET XACT_ABORT ON. Среди них инструкция SET XACT_ABORT ON наиболее важна. Если для кода на промышленной среде только на нее полагаться не стоит, то для быстрых и простых решений она вполне подходит.

Параметр NOCOUNT не имеет к обработке ошибок никакого отношения, но включение его в код является хорошей практикой. NOCOUNT подавляет сообщения вида (1 row(s) affected), которые вы можете видеть в панели Message в SQL Server Management Studio. В то время как эти сообщения могут быть полезны при работе c SSMS, они могут негативно повлиять на производительность в приложении, так как увеличивают сетевой трафик. Сообщение о количестве строк также может привести к ошибке в плохо написанных клиентских приложениях, которые могут подумать, что это данные, которые вернул запрос.

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

SET NOCOUNT ON
SET XACT_ABORT ON

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

3. Основной пример обработки ошибок

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

Для примера я буду использовать эту простую таблицу.

CREATE TABLE sometable(a int NOT NULL,
                       b int NOT NULL,
                       CONSTRAINT pk_sometable PRIMARY KEY(a, b))

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

CREATE PROCEDURE insert_data @a int, @b int AS 
   SET XACT_ABORT, NOCOUNT ON
   BEGIN TRY
      BEGIN TRANSACTION
      INSERT sometable(a, b) VALUES (@a, @b)
      INSERT sometable(a, b) VALUES (@b, @a)
      COMMIT TRANSACTION
   END TRY
   BEGIN CATCH
      IF @@trancount > 0 ROLLBACK TRANSACTION
      DECLARE @msg nvarchar(2048) = error_message()  
      RAISERROR (@msg, 16, 1)
      RETURN 55555
   END CATCH

Первая строка в процедуре включает XACT_ABORT и NOCOUNT в одном выражении, как я показывал выше. Эта строка – единственная перед BEGIN TRY. Все остальное в процедуре должно располагаться после BEGIN TRY: объявление переменных, создание временных таблиц, табличных переменных, всё. Даже если у вас есть другие SET-команды в процедуре (хотя причины для этого встречаются редко), они должны идти после BEGIN TRY.

Причина, по которой я предпочитаю указывать SET XACT_ABORT и NOCOUNT перед BEGIN TRY, заключается в том, что я рассматриваю это как одну строку шума: она всегда должна быть там, но я не хочу, чтобы это мешало взгляду. Конечно же, это дело вкуса, и если вы предпочитаете ставить SET-команды после BEGIN TRY, ничего страшного. Важно то, что вам не следует ставить что-либо другое перед BEGIN TRY.

Часть между BEGIN TRY и END TRY является основной составляющей процедуры. Поскольку я хотел использовать транзакцию, определенную пользователем, я ввел довольно надуманное бизнес-правило, в котором говорится, что если вы вставляете пару, то обратная пара также должна быть вставлена. Два выражения INSERT находятся внутри BEGIN и COMMIT TRANSACTION. Во многих случаях у вас будет много строк кода между BEGIN TRY и BEGIN TRANSACTION. Иногда у вас также будет код между COMMIT TRANSACTION и END TRY, хотя обычно это только финальный SELECT, возвращающий данные или присваивающий значения выходным параметрам. Если ваша процедура не выполняет каких-либо изменений или имеет только одно выражение INSERT/UPDATE/DELETE/MERGE, то обычно вам вообще не нужно явно указывать транзакцию.

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

  1. Откатывает любые открытые транзакции.
  2. Повторно вызывает ошибку.
  3. Убеждается, что возвращаемое процедурой значение отлично от нуля.

Эти три действия должны всегда быть там. Мы можете возразить, что строка

IF @@trancount > 0 ROLLBACK TRANSACTION

не нужна, если нет явной транзакции в процедуре, но это абсолютно неверно. Возможно, вы вызываете хранимую процедуру, которая открывает транзакцию, но которая не может ее откатить из-за ограничений TRY-CATCH. Возможно, вы или кто-то другой добавите явную транзакцию через два года. Вспомните ли вы тогда о том, что нужно добавить строку с откатом? Не рассчитывайте на это. Я также слышу читателей, которые возражают, что если тот, кто вызывает процедуру, открыл транзакцию, мы не должны ее откатывать… Нет, мы должны, и если вы хотите знать почему, вам нужно прочитать вторую и третью части. Откат транзакции в обработчике CATCH – это категорический императив, у которого нет исключений.

Код повторной генерации ошибки включает такую строку:

DECLARE @msg nvarchar(2048) = error_message()

Встроенная функция error_message() возвращает текст возникшей ошибки. В следующей строке ошибка повторно вызывается с помощью выражения RAISERROR. Это не самый простой способ вызова ошибки, но он работает. Другие способы мы рассмотрим в следующей главе.

Замечание: синтаксис для присвоения начального значения переменной в DECLARE был внедрен в SQL Server 2008. Если у вас SQL Server 2005, вам нужно разбить строку на DECLARE и выражение SELECT.

Финальное выражение RETURN – это страховка. RAISERROR никогда не прерывает выполнение, поэтому выполнение следующего выражения будет продолжено. Пока все процедуры используют TRY-CATCH, а также весь клиентский код обрабатывает исключения, нет повода для беспокойства. Но ваша процедура может быть вызвана из старого кода, написанного до SQL Server 2005 и до внедрения TRY-CATCH. В те времена лучшее, что мы могли делать, это смотреть на возвращаемые значения. То, что вы возвращаете с помощью RETURN, не имеет особого значения, если это не нулевое значение (ноль обычно обозначает успешное завершение работы).

Последнее выражение в процедуре – это END CATCH. Никогда не следует помещать какой-либо код после END CATCH. Кто-нибудь, читающий процедуру, может не увидеть этот кусок кода.

После прочтения теории давайте попробуем тестовый пример:

EXEC insert_data 9, NULL

Результат выполнения:

Msg 50000, Level 16, State 1, Procedure insert_data, Line 12
Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.

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

CREATE PROCEDURE outer_sp @a int, @b int AS
   SET XACT_ABORT, NOCOUNT ON
   BEGIN TRY
      EXEC insert_data @a, @b
   END TRY
   BEGIN CATCH
      IF @@trancount > 0 ROLLBACK TRANSACTION
      DECLARE @msg nvarchar(2048) = error_message()
      RAISERROR (@msg, 16, 1)
      RETURN 55555
   END CATCH
go
EXEC outer_sp 8, 8

Результат работы:

Msg 50000, Level 16, State 1, Procedure outer_sp, Line 9
Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

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

Msg 50000, Level 16, State 1, Procedure insert_data, Line 12
Msg 50000, Level 16, State 1, Procedure outer_sp, Line 9

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

4. Три способа генерации ошибки

4.1 Использование error_handler_sp

Мы рассмотрели функцию error_message(), которая возвращает текст сообщения об ошибке. Сообщение об ошибке состоит из нескольких компонентов, и существует своя функция error_xxx() для каждого из них. Мы можем использовать их для повторной генерации полного сообщения, которое содержит оригинальную информацию, хотя и в другом формате. Если делать это в каждом обработчике CATCH, это будет большой недостаток — дублирование кода. Вам не обязательно находиться в блоке CATCH для вызова error_message() и других подобных функций, и они вернут ту же самую информацию, если будут вызваны из хранимой процедуры, которую выполнит блок CATCH.

Позвольте представить вам error_handler_sp:

CREATE PROCEDURE error_handler_sp AS
 
   DECLARE @errmsg   nvarchar(2048),
           @severity tinyint,
           @state    tinyint,
           @errno    int,
           @proc     sysname,
           @lineno   int
           
   SELECT @errmsg = error_message(), @severity = error_severity(),
          @state  = error_state(), @errno = error_number(),
          @proc   = error_procedure(), @lineno = error_line()
       
   IF @errmsg NOT LIKE '***%'
   BEGIN
      SELECT @errmsg = '*** ' + coalesce(quotename(@proc), '<dynamic SQL>') + 
                       ', Line ' + ltrim(str(@lineno)) + '. Errno ' + 
                       ltrim(str(@errno)) + ': ' + @errmsg
   END
   RAISERROR('%s', @severity, @state, @errmsg)

Первое из того, что делает error_handler_sp – это сохраняет значение всех error_xxx() функций в локальные переменные. Я вернусь к выражению IF через секунду. Вместо него давайте посмотрим на выражение SELECT внутри IF:

SELECT @errmsg = '*** ' + coalesce(quotename(@proc), '<dynamic SQL>') + 
                 ', Line ' + ltrim(str(@lineno)) + '. Errno ' + 
                 ltrim(str(@errno)) + ': ' + @errmsg

Цель этого SELECT заключается в форматировании сообщения об ошибке, которое передается в RAISERROR. Оно включает в себя всю информацию из оригинального сообщения об ошибке, которое мы не можем вставить напрямую в RAISERROR. Мы должны обработать имя процедуры, которое может быть NULL для ошибок в обычных скриптах или в динамическом SQL. Поэтому используется функция COALESCE. (Если вы не понимаете форму выражения RAISERROR, я рассказываю о нем более детально во второй части.)

Отформатированное сообщение об ошибке начинается с трех звездочек. Этим достигаются две цели: 1) Мы можем сразу видеть, что это сообщение вызвано из обработчика CATCH. 2) Это дает возможность для error_handler_sp отфильтровать ошибки, которые уже были сгенерированы один или более раз, с помощью условия NOT LIKE ‘***%’ для того, чтобы избежать изменения сообщения во второй раз.

Вот как обработчик CATCH должен выглядеть, когда вы используете error_handler_sp:

BEGIN CATCH
   IF @@trancount > 0 ROLLBACK TRANSACTION
   EXEC error_handler_sp
   RETURN 55555
END CATCH

Давайте попробуем несколько тестовых сценариев.

EXEC insert_data 8, NULL
EXEC outer_sp 8, 8

Результат выполнения:

Msg 50000, Level 16, State 2, Procedure error_handler_sp, Line 20
*** [insert_data], Line 5. Errno 515: Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
Msg 50000, Level 14, State 1, Procedure error_handler_sp, Line 20
*** [insert_data], Line 6. Errno 2627: Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

Заголовки сообщений говорят о том, что ошибка возникла в процедуре error_handler_sp, но текст сообщений об ошибках дает нам настоящее местонахождение ошибки – как название процедуры, так и номер строки.

Я покажу еще два метода вызова ошибок. Однако error_handler_sp является моей главной рекомендацией для читателей, которые читают эту часть. Это — простой вариант, который работает на всех версиях SQL Server начиная с 2005. Существует только один недостаток: в некоторых случаях SQL Server генерирует два сообщения об ошибках, но функции error_xxx() возвращают только одну из них, и поэтому одно из сообщений теряется. Это может быть неудобно при работе с административными командами наподобие BACKUPRESTORE, но проблема редко возникает в коде, предназначенном чисто для приложений.

4.2. Использование ;THROW

В SQL Server 2012 Microsoft представил выражение ;THROW для более легкой обработки ошибок. К сожалению, Microsoft сделал серьезную ошибку при проектировании этой команды и создал опасную ловушку.

С выражением ;THROW вам не нужно никаких хранимых процедур. Ваш обработчик CATCH становится таким же простым, как этот:

BEGIN CATCH
   IF @@trancount > 0 ROLLBACK TRANSACTION
   ;THROW
   RETURN 55555
END CATCH

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

Если у вас SQL Server 2012 или более поздняя версия, измените определение insert_data и outer_sp и попробуйте выполнить тесты еще раз. Результат в этот раз будет такой:

Msg 515, Level 16, State 2, Procedure insert_data, Line 5
Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
Msg 2627, Level 14, State 1, Procedure insert_data, Line 6
Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

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

В этом месте вы можете сказать себе: действительно ли Microsoft назвал команду ;THROW? Разве это не просто THROW? На самом деле, если вы посмотрите в Books Online, там не будет точки с запятой. Но точка с запятой должны быть. Официально они отделяют предыдущее выражение, но это опционально, и далеко не все используют точку с запятой в выражениях T-SQL. Более важно, что если вы пропустите точку с запятой перед THROW, то не будет никакой синтаксической ошибки. Но это повлияет на поведение при выполнении выражения, и это поведение будет непостижимым для непосвященных. При наличии активной транзакции вы получите сообщение об ошибке, которое будет полностью отличаться от оригинального. И еще хуже, что при отсутствии активной транзакции ошибка будет тихо выведена без обработки. Такая вещь, как пропуск точки с запятой, не должно иметь таких абсурдных последствий. Для уменьшения риска такого поведения, всегда думайте о команде как о ;THROW (с точкой с запятой).

Нельзя отрицать того, что ;THROW имеет свои преимущества, но точка с запятой не единственная ловушка этой команды. Если вы хотите использовать ее, я призываю вас прочитать по крайней мере вторую часть этой серии, где я раскрываю больше деталей о команде ;THROW. До этого момента, используйте error_handler_sp.

4.3. Использование SqlEventLog

Третий способ обработки ошибок – это использование SqlEventLog, который я описываю очень детально в третьей части. Здесь я лишь сделаю короткий обзор.

SqlEventLog предоставляет хранимую процедуру slog.catchhandler_sp, которая работает так же, как и error_handler_sp: она использует функции error_xxx() для сбора информации и выводит сообщение об ошибке, сохраняя всю информацию о ней. Вдобавок к этому, она логирует ошибку в таблицу splog.sqleventlog. В зависимости от типа приложения, которое у вас есть, эта таблица может быть очень ценным объектом.

Для использования SqlEventLog, ваш обработчик CATCH должен быть таким:

BEGIN CATCH
   IF @@trancount > 0 ROLLBACK TRANSACTION
   EXEC slog.catchhandler_sp @@procid
   RETURN 55555
END CATCH

@@procid возвращает идентификатор объекта текущей хранимой процедуры. Это то, что SqlEventLog использует для логирования информации в таблицу. Используя те же тестовые сценарии, получим результат их работы с использованием catchhandler_sp:

Msg 50000, Level 16, State 2, Procedure catchhandler_sp, Line 125
{515} Procedure insert_data, Line 5
Cannot insert the value NULL into column 'b', table 'tempdb.dbo.sometable'; column does not allow nulls. INSERT fails.
Msg 50000, Level 14, State 1, Procedure catchhandler_sp, Line 125
{2627} Procedure insert_data, Line 6
Violation of PRIMARY KEY constraint 'pk_sometable'. Cannot insert duplicate key in object 'dbo.sometable'. The duplicate key value is (8, 8).

Как вы видите, сообщение об ошибке отформатировано немного не так, как это делает error_handler_sp, но основная идея такая же. Вот образец того, что было записано в таблицу slog.sqleventlog:

logid logdate errno severity logproc linenum msgtext
1 2015-01-25 22:40:24.393 515 16 insert_data 5 Cannot insert …
2 2015-01-25 22:40:24.395 2627 14 insert_data 6 Violation of …

Если вы хотите попробовать SqlEventLog, вы можете загрузить файл sqleventlog.zip. Инструкция по установке находится в третьей части, раздел Установка SqlEventLog.

5. Финальные замечания

Вы изучили основной образец для обработки ошибок и транзакций в хранимых процедурах. Он не идеален, но он должен работать в 90-95% вашего кода. Есть несколько ограничений, на которые стоит обратить внимание:

  1. Как мы видели, ошибки компиляции не могут быть перехвачены в той же процедуре, в которой они возникли, а только во внешней процедуре.
  2. Пример не работает с пользовательскими функциями, так как ни TRY-CATCH, ни RAISERROR нельзя в них использовать.
  3. Когда хранимая процедура на Linked Server вызывает ошибку, эта ошибка может миновать обработчик в хранимой процедуре на локальном сервере и отправиться напрямую клиенту.
  4. Когда процедура вызвана как INSERT-EXEC, вы получите неприятную ошибку, потому что ROLLBACK TRANSACTION не допускается в данном случае.
  5. Как упомянуто выше, если вы используете error_handler_sp или SqlEventLog, мы потеряете одно сообщение, когда SQL Server выдаст два сообщения для одной ошибки. При использовании ;THROW такой проблемы нет.

Я рассказываю об этих ситуациях более подробно в других статьях этой серии.

Перед тем как закончить, я хочу кратко коснуться триггеров и клиентского кода.

Триггеры

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

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

Клиентский код

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

Здесь я только обращу внимание на важную вещь: реакцией на ошибку, возвращенную SQL Server, должно быть завершение запроса во избежание открытых бесхозных транзакций:

IF @@trancount > 0 ROLLBACK TRANSACTION

Это также применимо к знаменитому сообщению Timeout expired (которое является не сообщением от SQL Server, а от API).

6. Конец первой части

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

… и не забывайте добавлять эту строку в начало ваших хранимых процедур:

SET XACT_ABORT, NOCOUNT ON

Автор: Schekotka

Источник

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

Что мы будем делать

В этой статье мы собираемся объяснить удобный для новичков способ обработки ошибок в API Node.js + Express.js с помощью TypeScript. Мы собираемся объяснить, что такое ошибка, различные типы ошибок, которые могут возникнуть, и как их обрабатывать в нашем приложении. Вот некоторые вещи, которыми мы займемся в следующих главах:

  1. узнаем, что на самом деле такое «обработка ошибок» и с какими типами ошибок вы столкнетесь
  2. узнаем об объекте Node.js Error и о том, как его использовать
  3. узнаем, как создавать собственные классы ошибок и как они могут помочь нам в разработке лучших API-интерфейсов и Node-приложений
  4. узнаем о промежуточном программном обеспечении Express и о том, как его использовать для обработки наших ошибок
  5. научимся структурировать информацию об ошибках и представлять ее потребителю и разработчику

Предусловие

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

  1. рабочее знание Node.js
  2. рабочее знание Express.js (маршруты, промежуточное ПО и т. д.)
  3. основы TypeScript (и классы!)
  4. основы работы API и его написания с использованием Express.js

Хорошо. Мы можем начинать.

Что такое обработка ошибок и зачем она вам нужна?

Так что же такое «обработка ошибок» на самом деле?

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

Зачем нужна обработка ошибок?

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

Типы ошибок

Есть два основных типа ошибок, которые нам необходимо различать и соответственно обрабатывать.

Операционные ошибки

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

  1. Запрос API не выполняется по какой-либо причине (например, сервер не работает или превышен лимит скорости)
  2. Невозможно установить соединение с базой данных
  3. Пользователь отправляет неверные входные данные
  4. Системе не хватает памяти

Ошибки программиста

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

  1. Попытка прочитать свойство объекта, которое не определено
  2. Передача некорректных параметров в функции
  3. не улавливая отвергнутого обещания

Что такое ошибка узла?

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

Мы можем создать такую ​​ошибку:

const error = new Error('Error message');

Итак, мы дали ему строковый параметр, который будет сообщением об ошибке. Но что еще есть в этом Error? Поскольку мы используем typescript, мы можем проверить его определение, что приведет нас к interface typescript:

const error = new Error('Error message');

Name и message не требуют пояснений, а stack содержит namemessage и строку, описывающую точку в коде, в которой был создан  Error. Этот стек на самом деле представляет собой серию стековых фреймов (подробнее о нем можно узнать здесь). Каждый фрейм описывает сайт вызова в коде, который приводит к сгенерированной ошибке. Мы можем вызвать стек console.log(),

и посмотрим, что он может нам сказать. Вот пример ошибки, которую мы получаем при передаче строки в качестве аргумента функции JSON.parse() (которая завершится ошибкой, поскольку JSON.parse() принимает только данные JSON в строковом формате):

Как мы видим, это ошибка типа SyntaxError с сообщением « Неожиданный токен A в JSON в позиции 0 ». Внизу мы видим кадры стека. Это ценная информация, которую мы, как разработчик, можем использовать для отладки нашего кода, определения проблемы и ее устранения.

Написание собственных классов ошибок.

Пользовательские классы ошибок

Как я упоминал ранее, мы можем использовать встроенный объект Error, поскольку он дает нам ценную информацию.

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

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

class BaseError extends Error {
  statusCode: number;

  constructor(statusCode: number, message: string) {
    super(message);

    Object.setPrototypeOf(this, new.target.prototype);
    this.name = Error.name;
    this.statusCode = statusCode;
    Error.captureStackTrace(this);
  }
}

Здесь мы создаем класс BaseError, расширяющий этот класс Error. Объект принимает statusCode(код состояния HTTP, который мы вернем пользователю) и message(сообщение об ошибке, как при создании встроенного объекта Node Error).

Теперь мы можем использовать класс Node BaseError вместо класса Error для добавления кода состояния HTTP.

// Import the class
import { BaseError } from '../utils/error';

const extendedError = new BaseError(400, 'message');

Мы будем использовать этот класс BaseError в качестве основы для всех наших пользовательских ошибок.

Теперь мы можем использовать класс BaseError, чтобы расширить его и сформировать все наши собственные ошибки. Это зависит от потребностей нашего приложения. Например, если мы собираемся иметь конечные точки аутентификации в нашем API, мы можем расширить класс BaseError и создать класс AuthenticationError:

class AuthenticationError extends BaseError {}

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

Теперь, когда мы знаем, как расширить объект Error, мы можем сделать еще один шаг.

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

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

Давайте расширим класс BaseError, но теперь давайте сделаем код состояния по умолчанию 404 и поместим аргумент «свойство» в конструктор:

class NotFoundError extends BaseError {
  propertyName: string;

  constructor(propertyName: string) {
    super(404, `Property '${propertyName}' not found.`);

    this.propertyName = propertyName;
  }
}

Теперь при использовании класса NotFoundError мы можем просто дать ему имя свойства, и объект создаст для нас полное сообщение (statusCode по умолчанию будет 404, как вы можете видеть из кода).

// This is how we can use the error
const notFoundError = new NotFoundError('Product');

А вот как это выглядит при сбросе:

Теперь мы можем создавать различные ошибки в соответствии с нашими потребностями. Вот некоторые из наиболее распространенных примеров API:

  1. ValidationError (ошибки, которые можно использовать при обработке входящих пользовательских данных)
  2. DatabaseError (ошибки, которые вы можете использовать, чтобы сообщить пользователю, что существует проблема с взаимодействием с базой данных)
  3. AuthenticationError (ошибка, которую можно использовать, чтобы сообщить пользователю об ошибке аутентификации)

Идем на шаг дальше

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

Например, вы можете использовать коды ошибок в AuthenticationError чтобы сообщить потребителю тип ошибки аутентификации. A01 может означать, что пользователь не проверен, а A02 может означать, что срок действия ссылки для сброса пароля истек.

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

Создание и обнаружение ошибок в контроллерах

Теперь давайте посмотрим на образец контроллера (функцию маршрута) в Express.js.

const sampleController = (req: Request, res: Response, next: NextFunction) => {

  res.status(200).json({
    response: 'successfull',
    data: {
      answer: 42
    }
  });
};

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

const sampleController = async (req: Request, res: Response, next: NextFunction) => {

    return next(new NotFoundError('Product'))

  res.status(200).json({
    response: 'successfull',
    data: {
      answer: 42
    }
  });
};

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

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

Необработанные ошибки

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

Допустим, вы хотите использовать функцию JSON.parse(). Эта функция принимает данные JSON в виде строки, но вы даете ей случайную строку. Если передать этой функции, основанной на обещаниях, строку, она выдаст ошибку! Если не обработать, это вызовет ошибку UnhandledPromiseRejectionWarning.

Что ж, просто оберните свой код в блок try/catch и передайте любые ошибки по строке промежуточного программного обеспечения, используя next()(опять же, я скоро объясню это)!

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

Оболочка handleAsync

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

Вот как это выглядит:

const asyncHandler = (fn: any) => (req: Request, res: Response, next: NextFunction) => Promise.resolve(fn(req, res, next)).catch(next);

На первый взгляд это может показаться сложным, но это всего лишь функция промежуточного программного обеспечения, которая действует как блок try /catch с next(err) внутри catch(). Теперь мы можем просто обернуть его вокруг наших контроллеров, и все!

const sampleController = asyncHandler(async (req: Request, res: Response, next: NextFunction) => {
  JSON.parse('A string');

  res.status(200).json({
    response: 'successfull',
    data: {
      something: 2
    }
  });
});

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

Как мне обрабатывать ошибки?

Хорошо, мы научились формировать ошибки. Что теперь?

Теперь нам нужно выяснить, как на самом деле с ними справиться.

Экспресс промежуточное ПО

Экспресс-приложение, по сути, представляет собой серию вызовов функций промежуточного программного обеспечения. Функция промежуточного программного обеспечения имеет доступ к объекту request, объекту response и функции next промежуточного программного обеспечения.

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

Выявление ошибок в Express

В Express есть специальный тип функции промежуточного программного обеспечения, называемый «промежуточное программное обеспечение для обработки ошибок». У этих функций есть дополнительный аргумент err. Каждый раз, когда в функции промежуточного программного обеспечения next() передается ошибка, Express пропускает все функции промежуточного программного обеспечения и сразу переходит к функциям обработки ошибок.

Вот пример того, как его написать:

const errorMiddleware = (error: any, req: Request, res: Response, next: NextFunction) => {
  // Do something with the error
  next(error); // pass it to the next function
};

Что делать с ошибками

Теперь, когда мы знаем, как обнаруживать ошибки, мы должны что-то с ними делать. В API обычно нужно сделать две вещи: ответить клиенту и записать ошибку.

errorReponse промежуточное ПО (ответ клиенту)

Лично при написании API я следую согласованной структуре ответов JSON для успешных и неудачных запросов:

// Success
{
    "response": "successfull",
    "message": "some message if required",
    "data": {}
}

// Failure
{
    "response": "error",
      "error": {
        "type": "type of error",
        "path": "/path/on/which/it/happened",
        "statusCode": 404,
        "message": "Message that describes the situation"
      }
}

А теперь мы собираемся написать промежуточное программное обеспечение, которое обрабатывает часть сбоя.

const errorResponse = (error: any, req: Request, res: Response, next: NextFunction) => {
  const customError: boolean = error.constructor.name === 'NodeError' || error.constructor.name === 'SyntaxError' ? false : true;

  res.status(error.statusCode || 500).json({
    response: 'Error',
    error: {
      type: customError === false ? 'UnhandledError' : error.constructor.name,
      path: req.path,
      statusCode: error.statusCode || 500,
      message: error.message
    }
  });
  next(error);
};

Давайте рассмотрим функцию. Сначала мы создаем логическое значение customError. Мы проверяем свойство error.constructor.name, которое сообщает нам, с какой ошибкой мы имеем дело. Если error.constructor.name это NodeError(или какая-то другая ошибка, которую мы не создавали лично), мы устанавливаем логическое значение false, в противном случае мы устанавливаем true. Таким образом, мы можем по-разному обрабатывать известные и неизвестные ошибки.

Далее мы можем ответить клиенту. Мы используем res.status() для установки кода состояния HTTP и используем функцию res.json() для отправки данных JSON клиенту. При записи данных JSON мы можем использовать логическое значение customError для установки определенных свойств. Например, если логическое значение false customError, мы установим тип ошибки UnhandledError, сообщая пользователю, что мы не ожидали этой ситуации, в противном случае мы устанавливаем его на error.constructor.name.

Поскольку свойство statusCode доступно только в наших настраиваемых объектах ошибок, мы можем просто вернуть 500, если оно недоступно (то есть это необработанная ошибка).

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

errorLog промежуточное ПО (регистрация ошибки)

const errorLogging = (error: any, req: Request, res: Response, next: NextFunction) => {
  const customError: boolean = error.constructor.name === 'NodeError' || error.constructor.name === 'SyntaxError' ? false : true;

  console.log('ERROR');
  console.log(`Type: ${error.constructor.name === 'NodeError' ? 'UnhandledError' : error.constructor.name}`);
  console.log('Path: ' + req.path);
  console.log(`Status code: ${error.statusCode || 500}`);
  console.log(error.stack);
};

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

Как вы можете видеть, это будет просто console.log() данные об ошибке в системной консоли. В большинстве производственных API ведение журнала немного более продвинуто, запись в файл или запись в API. Поскольку эта часть построения API очень специфична для конкретного приложения, я не хотел слишком углубляться в нее. Теперь, когда у вас есть данные, выберите, какой подход лучше всего подходит для вашего приложения, и реализуйте свою версию ведения журнала. Если вы развертываетесь в облачной службе развертывания, такой как AWS, вы сможете загружать файлы журналов, просто используя функцию промежуточного программного обеспечения, описанную выше (AWS сохраняет все файлы console.log().

Теперь вы можете обрабатывать ошибки.

Вот так! Этого должно быть достаточно, чтобы вы начали обрабатывать ошибки в рабочем процессе TypeScript + Node.js + Express.js API. Обратите внимание, здесь есть много возможностей для улучшения. Этот подход не является лучшим и не самым быстрым, но он довольно прост и, что наиболее важно, снисходителен, и его быстро можно итерировать и улучшать по мере развития вашего проекта API, требующего большего от ваших навыков. Эти концепции очень важны, и с ними легко начать, и я надеюсь, что вам понравилась моя статья и вы узнали что-то новое.

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

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

А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Обозначение ошибок на панели приборов газель бизнес
  • Обработка ошибок ввода вывода паскаль