Представьте: вы пытаетесь забронировать билет на концерт в 2 часа ночи, лишены кофеина, но полны решимости. Сайт выдаёт ошибку — «SYSTEM_ERR_CODE 0xDEADBEEF: Неверное выравнивание конденсатора потока». Внезапно вы боретесь не только с недосыпом, но и с экзистенциальным страхом. Вот почему обработка ошибок важнее, чем последний синтаксический сахар вашего любимого фреймворка.

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

1. Создайте свою таксономию ошибок как мастер покемонов

Подобно тому как Пикачу эволюционирует в Райчу, вашим ошибкам нужна правильная родословная. Давайте создадим иерархию ошибок, которой гордился бы Дарвин:

class ApplicationError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}
// Ошибки валидации — семейство «вы ошиблись»
class ValidationError extends ApplicationError {}
class RequiredFieldError extends ValidationError {
  constructor(field) {
    super(`Подождите! Нам нужно ваше ${field} для продолжения`);
    this.field = field;
  }
}
// Ошибки API — когда ударяют интернетные гремлины
class APIError extends ApplicationError {
  constructor(endpoint) {
    super(`Нашим хомячкам, работающим с ${endpoint}, нужен перерыв на кофе`);
    this.retryAfter = 30000;
  }
}

Теперь вы можете ловить конкретные ошибки как профессионал:

try {
  await submitForm();
} catch (error) {
  if (error instanceof RequiredFieldError) {
    highlightMissingField(error.field);
  } else if (error instanceof APIError) {
    showRetryButton(error.retryAfter);
  } else {
    // Наша финальная форма: неожиданные ошибки
    logToSentry(error);
    showPanicMessage();
  }
}
classDiagram Ошибка <|-- ApplicationError ApplicationError <|-- ValidationError ApplicationError <|-- APIError ValidationError <|-- RequiredFieldError ValidationError <|-- FormatError APIError <|-- TimeoutError APIError <|-- RateLimitError

Ваша таксономия ошибок должна расти как хорошо организованный набор инструментов, а не как ящик с кучей случайных USB-кабелей. Группируйте ошибки по доменам (валидация, API, аутентификация) и наследуйте их ответственно.

2. Пишите сообщения как человек, а не HAL 9000

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

  • What happened? (Что случилось?)
  • Action required (Требуется действие)
  • Instructions (необязательно) (Инструкции)
  • Tech details (скрыты) (Технические подробности) Хорошее, плохое и «WTF»:
// Плохо: «Неверный ввод»
// Хуже: «ERR_CODE 418: Переполнение чайника»
// Лучше: «Ой! В адресе электронной почты нужен символ @. Например, [email protected]»
class InvalidEmailError extends ValidationError {
  constructor(value) {
    super(`В адресе электронной почты нужен символ @. Мы получили: ${value}`);
    this.example = "[email protected]";
  }
}
// Бонус: добавьте устранение неполадок в объект ошибки
error.helpDocs = "https://example.com/email-format-guide";

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

3. Цирк обработки ошибок: Try/Catch/Finally Edition

Обработка ошибок похожа на трёхактное цирковое представление. Давайте разберём основные номера:

flowchart TD A[Try] --> B[Критическая операция] B --> C{Успешно?} C -->|Да| D[Продолжить] C -->|Нет| E[Поймать] E --> F[Обработать изящно] F --> G[Записать детали] G --> H[Очистить всё] E --> H A --> H[Наконец]

Пример из реальной жизни с очисткой ресурсов:

async function processOrder(userId) {
  let dbConnection;
  try {
    dbConnection = await connectToDatabase();
    const cart = await dbConnection.getCart(userId);
    if (!cart.items.length) {
      throw new ValidationError("Ваша корзина пуста, как холодильник студента");
    }
    const paymentResult = await processPayment(cart);
    return await createOrder(dbConnection, paymentResult);
  } catch (error) {
    if (error instanceof ValidationError) {
      await sendUserNotification(userId, error.message);
    } else {
      await logErrorToService(error);
      await triggerIncidentWorkflow(error);
    }
    throw error; // Пусть обработчики выше решат
  } finally {
    if (dbConnection) {
      await dbConnection.cleanup(); 
    }
  }
}

Обратите внимание, как мы:

  1. Инициализируем ресурсы вне блока try.
  2. Обрабатываем ожидаемые ошибки (валидацию).
  3. Выполняем очистку в блоке finally — важно для предотвращения утечек памяти.
  4. Повторно генерируем исключение для глобального обработчика.

4. Вскрытие ошибки: ведение журнала как команда CSI

Когда возникают ошибки, вам нужно больше улик, чем в романе Агаты Кристи. Реализуйте 5 Ws ведения журнала ошибок:

class ErrorLogger {
  static log(error) {
    const entry = {
      когда: new Date().toISOString(),
      что: error.message,
      где: error.stack?.split('\n')?.trim(),
      кто: getCurrentUser()?.id || 'аноним',
      почему: error.constructor.name,
      как: {
        код: error.code,
        дополнительно: error.context
      }
    };
    sendToLogService(entry);
    if (!error.isOperational) {
      triggerPagerDuty(entry);
    }
  }
}
// Использование
try {
  riskyOperation();
} catch (error) {
  ErrorLogger.log(error);
  throw error;
}

Ваши журналы должны рассказывать историю яснее, чем документальный фильм Netflix о реальных преступлениях. Включайте:

  • Контекст пользователя (без персональных данных).
  • Трассировку стека.
  • Код ошибки.
  • Сведения о среде.
  • Хлебные крошки (предыдущие действия).

5. Набор выживания пользователя: версия для фронтенда

В мире React аварийные границы — это ваши аварийные сигналы. Давайте создадим такой, который будет полезнее GPS с разряженными батареями:

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>⚠️ Ну ничего себе!</h2>
          <p>Этот компонент вышел из строя. Мы послали ниндзя кодирования, чтобы исправить это.</p>
          <button onClick={this.handleRetry}>Повторить</button>
          <details>
            <summary>Технические подробности для ботаников</summary>
            <code>{this.state.error?.toString()}</code>
          </details>
        </div>
      );
    }
    return this.props.children;
  }
}

Ключевые особенности:

  • Дружелюбное сообщение (без технических терминов).
  • Возможность восстановления.
  • Скрытые технические подробности.
  • Автоматическая отчётность об ошибках.

6. Искусство разработки, управляемой ошибками

Превратите ошибки в функции с этим циклом разработки:

  1. Отслеживайте распространённые ошибки в продакшне.
  2. Анализируйте закономерности (неверные входные данные? Неустойчивость API?).
  3. Предотвращайте с помощью:
    • Лучшей валидации.
    • Логики повторных попыток.
    • Обучения пользователей.
  4. Следите за улучшениями. Пример: если вы видите частые ошибки «Invalid Date» (недопустимая дата):
// До
<input type="text" name="birthdate">
// После
<DatePicker