- Обрабатывайте ошибки как профессионал, используя все лучшие практики
- Получение базового экспресс-приложения
- Обработка ошибок URL not found
- Обработка всех ошибок с помощью специального промежуточного ПО
- Пользовательский объект ошибки
- Давайте обработаем ошибки приложения
- Давайте улучшим это!
- Пример пользовательской ошибки
- Работайте с ошибками программистов.
- Обработка необработанных отказов обещаний.
- Дальнейшее улучшение
- Github Repo:
Обрабатывайте ошибки как профессионал, используя все лучшие практики
Обработка ошибок — один из самых важных аспектов любого приложения производственного уровня. Любой может написать код для успешных случаев. Только настоящие профессионалы заботятся об ошибках.
Сегодня мы научимся именно этому. Давайте погрузимся в процесс.
Во-первых, мы должны понять, что не все ошибки одинаковы. Давайте посмотрим, сколько типов ошибок может возникать в приложении.
- Ошибка, сгенерированная пользователем
- Аппаратный сбой
- Ошибка времени выполнения
- Ошибка базы данных
Мы увидим, как можно легко справиться с этими различными типами ошибок.
Получение базового экспресс-приложения
Выполните следующую команду, чтобы получить базовое экспресс-приложение, созданное с помощью 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