Перехват ошибок в обработчиках

В клиентской маршрутизации ошибки возникают не реже, чем на сервере: сбои при загрузке данных, неверные параметры URL, ошибки в логике рендеринга. В Page.js отсутствует встроенная сложная система обработки исключений, поэтому управление ошибками ложится на разработчика. Это даёт гибкость, но требует чёткой архитектуры.

Базовый принцип обработки ошибок

Page.js использует цепочку middleware-функций. Каждая функция принимает аргументы (ctx, next). Ошибка может возникнуть:

  • синхронно внутри обработчика
  • асинхронно (например, при fetch)
  • при передаче управления через next()

Простейший способ перехвата — использование конструкции try...catch:

page('/user/:id', async (ctx) => {
  try {
    const res = await fetch(`/api/users/${ctx.params.id}`);
    if (!res.ok) throw new Error('Ошибка загрузки пользователя');

    const user = await res.json();
    renderUser(user);
  } catch (err) {
    renderError(err.message);
  }
});

Централизованный middleware для ошибок

Повторение try...catch в каждом маршруте быстро становится избыточным. Эффективнее вынести обработку в отдельный middleware.

function errorHandler(ctx, next) {
  try {
    return next();
  } catch (err) {
    handleError(err);
  }
}

Подключение:

page(errorHandler);

page('/dashboard', (ctx) => {
  throw new Error('Ошибка в dashboard');
});

Однако такой подход работает только для синхронных ошибок. Асинхронные требуют иной стратегии.

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

При использовании async/await ошибки не попадают в внешний try...catch, если next() возвращает Promise. Поэтому необходимо явно обрабатывать Promise:

function asyncErrorHandler(ctx, next) {
  return Promise.resolve(next()).catch((err) => {
    handleError(err);
  });
}

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

page(asyncErrorHandler);

page('/data', async () => {
  const res = await fetch('/api/data');
  if (!res.ok) throw new Error('Ошибка API');
});

Ошибки в цепочке middleware

Page.js позволяет выстраивать несколько обработчиков:

page('/profile/:id',
  loadUser,
  checkPermissions,
  renderProfile
);

Каждый этап может выбросить ошибку. Важно:

  • не подавлять ошибки без необходимости
  • передавать управление корректно

Пример:

async function loadUser(ctx, next) {
  const res = await fetch(`/api/users/${ctx.params.id}`);
  if (!res.ok) throw new Error('Пользователь не найден');

  ctx.user = await res.json();
  return next();
}

Если ошибка возникает, выполнение цепочки прерывается.

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

Для сложных приложений полезно стандартизировать ошибки:

class AppError extends Error {
  constructor(message, status = 500) {
    super(message);
    this.status = status;
  }
}

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

if (!user) {
  throw new AppError('Пользователь не найден', 404);
}

Централизованный обработчик:

function handleError(err) {
  if (err instanceof AppError) {
    renderErrorPage(err.status, err.message);
  } else {
    renderErrorPage(500, 'Внутренняя ошибка');
  }
}

Обработка 404 (не найдено)

Page.js предоставляет fallback через page('*', ...):

page('*', () => {
  renderErrorPage(404, 'Страница не найдена');
});

Важно размещать этот маршрут последним, иначе он перехватит все запросы.

Ошибки навигации

Иногда ошибка возникает при переходе, например, из-за некорректного URL:

page('/product/:id', (ctx) => {
  const id = Number(ctx.params.id);

  if (isNaN(id)) {
    throw new Error('Некорректный ID');
  }

  renderProduct(id);
});

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

Глобальный обработчик ошибок браузера

Для перехвата непредвиденных ошибок:

window.addEventListener('error', (event) => {
  handleError(event.error);
});

window.addEventListener('unhandledrejection', (event) => {
  handleError(event.reason);
});

Это особенно важно при работе с Promise.

Логирование ошибок

Ошибки желательно не только отображать, но и логировать:

function handleError(err) {
  console.error(err);

  sendToMonitoring(err);

  renderErrorPage(500, err.message);
}

Где sendToMonitoring может отправлять данные на сервер или в систему мониторинга.

Разделение пользовательских и системных ошибок

  • пользовательские ошибки (неверный ввод, 404)
  • системные ошибки (сбой API, баги)

Пример:

if (!inputValue) {
  throw new AppError('Поле обязательно', 400);
}

Системные ошибки не должны раскрывать внутреннюю информацию.

Асинхронные middleware-цепочки и ошибки

При смешивании синхронных и асинхронных middleware важно соблюдать единый стиль:

page(async (ctx, next) => {
  await next();
});

Иначе ошибки могут “теряться” и не доходить до обработчика.

Поведение при ошибках и UX

  • отображение fallback-страниц
  • сохранение состояния приложения
  • возможность повторить действие

Пример:

function renderErrorPage(status, message) {
  document.body.innerHTML = `
    <h1>${status}</h1>
    <p>${message}</p>
    <button oncl ick="location.reload()">Обновить</button>
  `;
}

Изоляция ошибок компонентов

Если используется UI-фреймворк (React, Vue), ошибки могут возникать на уровне компонентов. В таком случае:

  • Page.js обрабатывает маршрутизацию
  • UI-фреймворк — ошибки рендеринга

Важно не смешивать уровни ответственности.

Частые ошибки при работе с Page.js

  • отсутствие обработки Promise
  • забытый return next()
  • глобальный обработчик без учёта async
  • перехват ошибок без логирования
  • неправильный порядок middleware

Рекомендуемая архитектура

  1. Глобальный async error middleware
  2. Стандартизированные классы ошибок
  3. Валидация параметров маршрутов
  4. Отдельный fallback для 404
  5. Логирование и мониторинг

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