Обработка ошибок в JavaScript: try/catch, async/await и продвинутые паттерны

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Продвинутая обработка ошибок в JavaScript: от try/catch до глобальных перехватчиков

Обработка ошибок — это не просто «обёртывание кода в try/catch». В реальных продакшн-приложениях на Node.js грамотное управление исключениями позволяет избежать падений сервера, корректно логировать проблемы и давать пользователю понятные сообщения. В этой статье мы разберём продвинутые техники обработки ошибок в JavaScript, которые выходят за рамки базового синтаксиса.

1. Глубокое погружение в try/catch и finally

Классическая конструкция try...catch...finally остаётся основой синхронной обработки. Однако на продвинутом уровне важно понимать нюансы её работы.

function riskyOperation(value) {
  if (typeof value !== 'number') {
    throw new TypeError('Argument must be a number');
  }
  if (value < 0) {
    throw new RangeError('Value cannot be negative');
  }
  return Math.sqrt(value);
}

try {
  const result = riskyOperation(-4);
  console.log('Result:', result);
} catch (error) {
  if (error instanceof TypeError) {
    console.error('Type Error:', error.message);
  } else if (error instanceof RangeError) {
    console.error('Range Error:', error.message);
  } else {
    console.error('Unknown error:', error);
  }
} finally {
  console.log('Cleanup: always executed');
}

Ключевые моменты:

  • Блок finally выполняется всегда, даже если в try или catch есть return.
  • Используйте встроенные классы ошибок (TypeError, RangeError, SyntaxError) для точной идентификации.
  • Не перехватывайте ошибки, которые вы не можете обработать — дайте им всплыть выше.

2. Кастомные классы ошибок и расширение Error

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

class AppError extends Error {
  constructor(message, statusCode, code) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.code = code;
    Error.captureStackTrace(this, this.constructor);
  }
}

class ValidationError extends AppError {
  constructor(message, field) {
    super(message, 400, 'VALIDATION_ERROR');
    this.field = field;
  }
}

class DatabaseError extends AppError {
  constructor(message, query) {
    super(message, 500, 'DB_ERROR');
    this.query = query;
  }
}

// Использование
try {
  throw new ValidationError('Email is required', 'email');
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Field: ${error.field}, Status: ${error.statusCode}`);
  }
}

Почему это важно: кастомные ошибки позволяют централизованно обрабатывать разные сценарии (например, в middleware Express.js) и возвращать клиенту структурированный JSON.

3. Обработка ошибок в асинхронном коде: async/await и Promise

В Node.js большая часть кода асинхронна. Ошибки в Promise и async/await требуют особого подхода.

3.1. Обработка Promise

function fetchUserData(userId) {
  return new Promise((resolve, reject) => {
    if (userId <= 0) {
      reject(new RangeError('Invalid user ID'));
    }
    // имитация запроса к БД
    setTimeout(() => resolve({ id: userId, name: 'Alice' }), 100);
  });
}

fetchUserData(-1)
  .then(user => console.log(user))
  .catch(error => {
    console.error('Promise rejected:', error.message);
  })
  .finally(() => console.log('Cleanup after promise'));

3.2. Обработка async/await

async function getUserProfile(userId) {
  try {
    const user = await fetchUserData(userId);
    const posts = await fetchUserPosts(user.id);
    return { user, posts };
  } catch (error) {
    // Логируем и пробрасываем дальше
    console.error('Failed to get user profile:', error);
    throw new AppError('Unable to load profile', 500, 'PROFILE_ERROR');
  }
}

// Использование с обработкой на верхнем уровне
(async () => {
  try {
    const profile = await getUserProfile(1);
    console.log(profile);
  } catch (error) {
    console.error('Top-level error:', error.message);
  }
})();

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

4. Глобальные обработчики ошибок в Node.js

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

// Неперехваченные исключения (uncaughtException)
process.on('uncaughtException', (error, origin) => {
  console.error('UNCAUGHT EXCEPTION:', error);
  console.error('Origin:', origin);
  // Важно: завершить процесс после логирования
  process.exit(1);
});

// Необработанные отклонения Promise (unhandledRejection)
process.on('unhandledRejection', (reason, promise) => {
  console.error('UNHANDLED REJECTION:', reason);
  // В Node.js 15+ это приведёт к завершению процесса
});

// Пример, который вызовет unhandledRejection
setTimeout(() => {
  Promise.reject(new Error('This error is not handled'));
}, 100);

Важно: uncaughtException — это крайняя мера. После его срабатывания приложение может находиться в нестабильном состоянии. Рекомендуется логировать ошибку и завершать процесс, используя менеджер процессов (PM2, Docker), который перезапустит приложение.

5. Продвинутые паттерны обработки

5.1. Функция-обёртка для async middleware

// Для Express.js
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Использование
app.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) {
    throw new AppError('User not found', 404, 'NOT_FOUND');
  }
  res.json(user);
}));

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

// Middleware ошибок в Express
app.use((err, req, res, next) => {
  const status                

Блоги

Рекомендаций по книгам