Error handling

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

Зачем нужно

Ошибки неизбежны: сеть недоступна, пользователь ввёл невалидные данные, API вернул неожиданный формат. Грамотная обработка ошибок — разница между рабочим приложением и падающим.

Где используется

  • Сетевые запросы (fetch, API)
  • Парсинг данных (JSON.parse)
  • Работа с DOM (элемент не найден)
  • Валидация ввода
  • Файловые операции

Предпосылки

Promise, async-await

try/catch/finally

try {
  const data = JSON.parse('invalid json');
} catch (error) {
  console.error('Ошибка парсинга:', error.message);
} finally {
  console.log('Выполнится в любом случае');
}

// catch получает объект ошибки
try {
  undeclaredVar;
} catch (error) {
  console.log(error.name);    // "ReferenceError"
  console.log(error.message); // "undeclaredVar is not defined"
  console.log(error.stack);   // полный стек вызовов
}

// catch без переменной (ES2019)
try {
  riskyOperation;
} catch {
  // Ошибка не нужна — просто обработали
  fallbackOperation;
}

Типы встроенных ошибок

// Error — базовый тип
new Error('Что-то пошло не так');

// TypeError — операция с неправильным типом
null.toString();             // TypeError
undefined.prop;              // TypeError

// ReferenceError — обращение к несуществующей переменной
console.log(notDeclared);   // ReferenceError

// SyntaxError — ошибка синтаксиса
JSON.parse('{invalid}');    // SyntaxError

// RangeError — значение вне допустимого диапазона
new Array(-1);              // RangeError
(1).toFixed(200);           // RangeError

// URIError — неправильный URI
decodeURIComponent('%');    // URIError

// AggregateError (ES2021) — множество ошибок
Promise.any([
  Promise.reject(new Error('1')),
  Promise.reject(new Error('2'))
]).catch(err => {
  console.log(err instanceof AggregateError); // true
  console.log(err.errors); // [Error('1'), Error('2')]
});

throw — генерация ошибок

// Бросить ошибку
throw new Error('Пользователь не найден');

// Можно бросить любое значение (но лучше Error)
throw 'строка ошибки';      // Работает, но нет стека
throw 42;                    // Работает, но бесполезно
throw { code: 404, msg: 'Not found' }; // Нет стека

// Всегда используй new Error или его наследников
throw new TypeError('Ожидалось число, получена строка');

Кастомные ошибки

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

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

class NotFoundError extends AppError {
  constructor(entity, id) {
    super(`${entity} с id ${id} не найден`, 'NOT_FOUND');
    this.name = 'NotFoundError';
    this.entity = entity;
    this.entityId = id;
  }
}

// Использование
function getUser(id) {
  const user = db.find(u => u.id === id);
  if (!user) throw new NotFoundError('User', id);
  return user;
}

try {
  const user = getUser(999);
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log(`${error.entity} не найден`);
  } else if (error instanceof ValidationError) {
    console.log(`Ошибка валидации: ${error.field}`);
  } else {
    throw error; // Неизвестная ошибка — пробрасываем дальше
  }
}

Ошибки в асинхронном коде

Promise .catch

fetch('/api/data')
  .then(response => response.json())
  .then(data => {
    throw new Error('Ошибка обработки');
  })
  .catch(error => {
    // Ловит ошибки от ВСЕХ .then выше
    console.error(error.message);
  });

async/await + try/catch

async function loadData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) {
      console.error('Сетевая ошибка');
    } else {
      console.error('Ошибка:', error.message);
    }
    return null;
  }
}

Глобальные обработчики

// Браузер: необработанные ошибки
window.addEventListener('error', (event) => {
  console.error('Глобальная ошибка:', event.message);
  // Отправить в сервис мониторинга
});

// Браузер: необработанные rejected Promise
window.addEventListener('unhandledrejection', (event) => {
  console.error('Необработанный Promise:', event.reason);
  event.preventDefault(); // Предотвратить вывод в консоль
});

// Node.js
process.on('uncaughtException', (error) => {
  console.error('Uncaught:', error);
  process.exit(1); // Рекомендуется завершить процесс
});

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
});

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

Result-паттерн (без исключений)

function safeParse(json) {
  try {
    return { ok: true, value: JSON.parse(json) };
  } catch (error) {
    return { ok: false, error: error.message };
  }
}

const result = safeParse('{"a": 1}');
if (result.ok) {
  console.log(result.value);
} else {
  console.error(result.error);
}

Обёртка для async-функций

function catchAsync(fn) {
  return function(...args) {
    return fn(...args).catch(error => {
      console.error('Async error:', error);
    });
  };
}

const safeLoad = catchAsync(async (url) => {
  const res = await fetch(url);
  return res.json();
});

Частые ошибки

1. Поглощение ошибок

// Плохо: ошибка молча проглочена
try {
  riskyOperation;
} catch (e) {
  // Пустой catch — ошибка потеряна!
}

// Хорошо: как минимум логируй
try {
  riskyOperation;
} catch (e) {
  console.error(e);
}

2. Ловить слишком широко

// Плохо: ловим ВСЕ ошибки одинаково
try {
  doStuff;
} catch (e) {
  alert('Ошибка!');
}

// Хорошо: различай типы ошибок
try {
  doStuff;
} catch (e) {
  if (e instanceof ValidationError) {
    showFieldError(e.field);
  } else {
    throw e; // Пробросить неизвестные ошибки
  }
}

Практика

  1. Создай 3 кастомных класса ошибок с наследованием
  2. Напиши обёртку для fetch с правильной обработкой всех типов ошибок
  3. Реализуй глобальный обработчик ошибок с отправкой в «сервис мониторинга»
  4. Напиши функцию safeParse для JSON с Result-паттерном

Связанные темы

Ресурсы


🎓 Источник: Необработанные ошибки в промисах на Node.js

  • 📅 2019-03-07 · YouTube
  • Тезисы:
    • unhandledRejection — событие на process, срабатывает, когда промис отклонён и .catch не повешен в текущем тике.
    • rejectionHandled — если .catch повесили ПОЗЖЕ (асинхронно). Обычно это симптом ошибки архитектуры.
    • С Node 15+ unhandled rejection по умолчанию завершает процесс (--unhandled-rejections=throw).
    • Лечение: всегда await + try/catch, либо явный .catch, либо Promise.allSettled для несвязанных задач.
    • В браузере — window.addEventListener('unhandledrejection', e => ...), можно e.preventDefault() чтобы подавить вывод в консоль.

🎓 Источник: Проблема асинхронного стектрейса в JavaScript и Node.js

  • 📅 2019-03-21 · YouTube
  • Тезисы:
    • До Node 12 / V8 7.2 цепочка async-вызовов в stack trace терялась.
    • Throw из callback'а в setTimeout(cb) не ловится try/catch снаружи — он будет unhandled error в event loop.
    • С --async-stack-traces (включён по умолчанию в Node 12+) await-цепочка восстанавливается.
    • Throw из async-функции автоматически становится rejected Promise — try/catch с await ловит.
  • См. отдельную заметку Async stack trace.