Обрабатывайте ошибки в приложении NodeJS как профессионал!


Обрабатывайте ошибки как профессионал, используя все лучшие практики

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

Сегодня мы научимся именно этому. Давайте погрузимся в процесс.

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

  • Ошибка, сгенерированная пользователем
  • Аппаратный сбой
  • Ошибка времени выполнения
  • Ошибка базы данных

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

Получение базового экспресс-приложения

Выполните следующую команду, чтобы получить базовое экспресс-приложение, созданное с помощью typescript.

git clone https://github.com/Mohammad-Faisal/express-typescript-skeleton.git
Войти в полноэкранный режим Выйти из полноэкранного режима

Обработка ошибок URL not found

Как определить, что найденный URL не активен в вашем экспресс-приложении? У вас есть URL типа /users, но кто-то переходит по адресу /user. Нам нужно сообщить им, что URL, к которому они пытаются обратиться, не существует.

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

app.use("*", (req: Request, res: Response) => {
  const err = Error(`Requested path ${req.path} not found`);
  res.status(404).send({
    success: false,
    message: "Requested path ${req.path} not found",
    stack: err.stack,
  });
});
Вход в полноэкранный режим Выход из полноэкранного режима

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

Обработка всех ошибок с помощью специального промежуточного ПО

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

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

Давайте добавим его в наш индексный файл.

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  const statusCode = 500;
  res.status(statusCode).send({
    success: false,
    message: err.message,
    stack: err.stack,
  });
});
Вход в полноэкранный режим Выход из полноэкранного режима

Взгляните на сигнатуру промежуточного ПО. Как и другие промежуточные программы, эта специальная промежуточная программа имеет дополнительный параметр err, который имеет тип Error. Он передается в качестве первого параметра.

Измените наш предыдущий код, чтобы передать ошибку следующим образом.

app.use("*", (req: Request, res: Response, next: NextFunction) => {
  const err = Error(`Requested path ${req.path} not found`);
  next(err);
});
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, если мы нажмем на случайный URL, например http://localhost:3001/posta, то получим правильный ответ об ошибке со стеком.

{
  "success": false,
  "message": "Requested path ${req.path} not found",
  "stack": "Error: Requested path / not foundn    at /Users/mohammadfaisal/Documents/learning/express-typescript-skeleton/src/index.ts:23:15n"
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Давайте рассмотрим объект ошибки по умолчанию, предоставляемый NodeJS.

interface Error {
  name: string;
  message: string;
  stack?: string;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Итак, когда вы выбрасываете ошибку, подобную следующей.

throw new Error("Some message");
Войти в полноэкранный режим Выход из полноэкранного режима

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

Но мы можем захотеть добавить еще немного информации в сам объект ошибки.

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

Давайте разработаем базовый класс Custom error для нашего приложения.

export class ApiError extends Error {
  statusCode: number;
  constructor(statusCode: number, message: string) {
    super(message);

    this.statusCode = statusCode;
    Error.captureStackTrace(this, this.constructor);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание на следующую строку.

Error.captureStackTrace(this, this.constructor);
Войти в полноэкранный режим Выход из полноэкранного режима

Это помогает захватить стек-трассировку ошибки из любой точки приложения.

В этом простом классе мы можем также добавить statusCode.
Давайте изменим наш предыдущий код следующим образом.

app.use("*", (req: Request, res: Response, next: NextFunction) => {
  const err = new ApiError(404, `Requested path ${req.path} not found`);
  next(err);
});
Вход в полноэкранный режим Выйти из полноэкранного режима

И воспользуемся преимуществами нового свойства statusCode в промежуточном ПО обработчика ошибок

app.use((err: ApiError, req: Request, res: Response, next: NextFunction) => {
  const statusCode = err.statusCode || 500; // <- Look here

  res.status(statusCode).send({
    success: false,
    message: err.message,
    stack: err.stack,
  });
});
Вход в полноэкранный режим Выход из полноэкранного режима

Наличие пользовательского класса Error делает ваш API предсказуемым для конечных пользователей. Большинство новичков пропускают эту часть.

Давайте обработаем ошибки приложения

Теперь давайте бросим пользовательскую ошибку изнутри наших маршрутов.

app.get("/protected", async (req: Request, res: Response, next: NextFunction) => {
  try {
    throw new ApiError(401, "You are not authorized to access this!"); // <- fake error
  } catch (err) {
    next(err);
  }
});
Вход в полноэкранный режим Выход из полноэкранного режима

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

Если мы перейдем по следующему URL http://localhost:3001/protected, то получим следующий ответ.

{
  "success": false,
  "message": "You are not authorized to access this!",
  "stack": "Some details"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Итак, наш ответ на ошибку работает правильно!

Давайте улучшим это!

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

Это не идеальный вариант. Это быстро приведет наш код в неприглядный вид.

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

Давайте создадим утилиту-обертку для этой цели!

import { Request, Response, NextFunction } from "express";

export const asyncWrapper = (fn: any) => (req: Request, res: Response, next: NextFunction) => {
  Promise.resolve(fn(req, res, next)).catch((err) => next(err));
};
Вход в полноэкранный режим Выход из полноэкранного режима

И использовать его внутри нашего маршрутизатора.

import { asyncWrapper } from "./utils/asyncWrapper";

app.get(
  "/protected",
  asyncWrapper(async (req: Request, res: Response) => {
    throw new ApiError(401, "You are not authorized to access this!");
  })
);
Вход в полноэкранный режим Выход из полноэкранного режима

Запустите код и убедитесь, что мы получили те же результаты. Это поможет нам избавиться от всех блоков try/catch и вызывать следующую функцию везде!

Пример пользовательской ошибки

Мы можем точно настроить наши ошибки под свои нужды. Давайте создадим новый класс ошибки для маршрутов not found.

export class NotFoundError extends ApiError {
  constructor(path: string) {
    super(404, `The requested path ${path} not found!`);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

И упростим наш обработчик ненайденных маршрутов.

app.use((req: Request, res: Response, next: NextFunction) => next(new NotFoundError(req.path)));
Вход в полноэкранный режим Выход из полноэкранного режима

Насколько это чисто?

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

yarn add http-status-codes
Вход в полноэкранный режим Выйти из полноэкранного режима

И добавим код состояния в осмысленном виде.

export class NotFoundError extends ApiError {
  constructor(path: string) {
    super(StatusCodes.NOT_FOUND, `The requested path ${path} not found!`);
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

И внутри нашего маршрута вот так.

app.get(
  "/protected",
  asyncWrapper(async (req: Request, res: Response) => {
    throw new ApiError(StatusCodes.UNAUTHORIZED, "You are not authorized to access this!");
  })
);
Войти в полноэкранный режим Выход из полноэкранного режима

Это просто делает наш код немного лучше.

Работайте с ошибками программистов.

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

process.on("uncaughtException", (err: Error) => {
  console.log(err.name, err.message);
  console.log("UNCAUGHT EXCEPTION! 💥 Shutting down...");

  process.exit(1);
});
Войти в полноэкранный режим Выйти из полноэкранного режима

Обработка необработанных отказов обещаний.

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

process.on("unhandledRejection", (reason: Error, promise: Promise<any>) => {
  console.log(reason.name, reason.message);
  console.log("UNHANDLED REJECTION! 💥 Shutting down...");
  process.exit(1);
  throw reason;
});
Вход в полноэкранный режим Выход из полноэкранного режима

Дальнейшее улучшение

Давайте создадим новый класс ErrorHandler, чтобы обрабатывать ошибки в одном месте.

import { Request, Response, NextFunction } from "express";
import { ApiError } from "./ApiError";

export default class ErrorHandler {
  static handle = () => {
    return async (err: ApiError, req: Request, res: Response, next: NextFunction) => {
      const statusCode = err.statusCode || 500;
      res.status(statusCode).send({
        success: false,
        message: err.message,
        rawErrors: err.rawErrors ?? [],
        stack: err.stack,
      });
    };
  };
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

app.use(ErrorHandler.handle());
Вход в полноэкранный режим Выход из полноэкранного режима

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

Надеюсь, сегодня вы узнали что-то новое. Удачного вам отдыха!

Свяжитесь со мной на моем LinkedIN

Читайте больше статей на моем сайте

Github Repo:

https://github.com/Mohammad-Faisal/nodejs-expressjs-error-handling

Оцените статью
devanswers.ru
Добавить комментарий