Кто не сталкивался с загадочным 500 Internal Server Error
в продакшене? Нелогичные отказы, недоступность критичных функций, оповещения Slack в 3 часа ночи — всё это может быть следством пробелов в обработке ошибок. Разработка надёжных систем распределённых систем требует вдумчивого подхода, особенно в обработке ошибок. Рассмотрим эволюцию полноценной стратегии обработки ошибок в backend-разработке.
Фундамент: Почему стандартная обработка недостаточна
Примитивный подход к обработке ошибок часто выглядит так:
app.POST("/users", func(c *gin.Context) {
var user User
if err := c.BindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": "invalid email"})
return
}
if err := db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"error": "something went wrong"})
return
}
c.JSON(201, user)
})
При всей кажущейся понятности, здесь есть четыре фундаментальные проблемы:
- Утечка деталей ошибки (SQL-ошибки могут экспортироваться клиенту)
- Отсутствует контекст вызова (какая операция вызвала сбой?)
- Нет категоризации ошибок
- Невозможна централизованная обработка
Стратегия: многослойный подход
Создадим файл error_utils.go
с компонентами для обработки ошибок:
package errors
type ErrorCode int
const (
ECONFLICT ErrorCode = 409 // Конфликт данных
EINTERNAL ErrorCode = 500 // Внутренняя ошибка
ENOTFOUND ErrorCode = 404 // Не найдено
EINVALID ErrorCode = 400 // Невалидные данные
)
type AppError struct {
Code ErrorCode
Message string
Operation string
Err error
Metadata map[string]any
}
func Error(op string, err error, code ErrorCode, msg string) *AppError {
return &AppError{
Operation: op,
Err: err,
Code: code,
Message: msg,
}
}
func Wrap(op string, err error) *AppError {
if appErr, ok := err.(*AppError); ok {
return appErr
}
return &AppError{
Operation: op,
Err: err,
Code: EINTERNAL,
}
}
func (e *AppError) Error() string {
return fmt.Sprintf("%s [%s]: %v (code %d)",
e.Operation, e.Message, e.Err, e.Code)
}
func (e *AppError) Unwrap() error {
return e.Err
}
Практика: применение в бизнес-логике
Используем структурированную обработку в сервисном слое:
package user
func (s *Service) CreateUser(ctx context.Context, input *CreateUserInput) (*User, error) {
const op = "user.Service.CreateUser"
if err := validateUserInput(input); err != nil {
return nil, errors.Error(op, err, errors.EINVALID, "invalid user data")
}
user, err := toUserEntity(input)
if err != nil {
return nil, errors.Error(op, err, errors.EINTERNAL, "conversion error")
}
if err := s.repo.CreateUser(ctx, user); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, errors.Error(op, err, errors.ENOTFOUND, "user not found")
}
if strings.Contains(err.Error(), "duplicate") {
return nil, errors.Error(op, err, errors.ECONFLICT, "email already exists")
}
return nil, errors.Wrap(op, err)
}
return entityToDomain(user), nil
}
Централизованное обрамление: middleware как PPE
Обработчик Gin/Gonic для ошибок:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // Выполняем запрос
if len(c.Errors) > 0 {
err := c.Errors.Last().Err
appErr, ok := err.(*errors.AppError)
if !ok {
appErr = &errors.AppError{
Code: errors.EINTERNAL,
Message: "Unknown error",
Err: err,
}
}
// Логируем детали для себя
log.Printf("HTTP_ERROR | %s | %v\n", appErr.Operation, appErr.Err)
// Клиентам даём очищенную информацию
response := map[string]interface{}{
"code": appErr.Code,
"message": appErr.Message,
}
status := http.StatusInternalServerError
if appErr.Code == errors.EINVALID {
status = http.StatusBadRequest
}
// ... остальные коды статуса
c.JSON(status, response)
}
}
}
Реальный кейс: обработка потоковых данных
Для асинхронных обработчиков с Kafka или RabbitMQ требуется другой уровень обработки ошибок:
func (c *Consumer) HandleRegistrationEvent(event Event) error {
const op = "kafka.HandleRegistrationEvent"
payload, err := parseEvent(event.Payload)
if err != nil {
metrics.ErrorCount.WithLabelValues("parse").Inc()
return errors.Error(op, err, errors.EINVALID, "invalid payload")
}
if err := c.userService.SendWelcomeEmail(payload.Email); err != nil {
// Если не можем отправить письмо - логируем, но не прерываем
log.Printf("Failed send email: %v", err)
metrics.ErrorCount.WithLabelValues("email").Inc()
// Не возвращаем ошибку - хотим избежать повтора события?
return nil
}
return nil
}
Прикладная ошибкоустойчивость: паттерны восстановления
- Повторные попытки с экспоненциальными откатами (retry with backoff):
func DoWithRetry(
fn func() error,
delays ...time.Duration,
) error {
maxAttempts := len(delays)
for attempt := 0; attempt < maxAttempts; attempt++ {
if err := fn(); err == nil {
return nil
}
time.Sleep(delays[attempt])
}
return ErrServiceUnavailable
}
// Использование
err := DoWithRetry(func() error {
return paymentProvider.Charge(cardToken)
},
0, 500*time.Millisecond, 2*time.Second, 5*time.Second)
- Прерыватели (Circuit Breakers):
breaker := circuitbreaker.NewCircuitBreaker(0.95)
err := breaker.Execute(func() (interface{}, error) {
return nil, paymentProvider.Charge(cardToken)
})
if err != nil {
if circuitbreaker.IsOpen(err) {
cacheFallbackPayment()
}
return err
}
Тестирование ошибок: делаем тесты значимыми
Не доверяйте коду обработки ошибок без тестирования:
func TestCreateUser_DuplicateEmail(t *testing.T) {
// Случайное имя БД для изоляции теста
db := testDB()
s := NewService(db)
input := &CreateUserInput{Email: "test@example.com"}
_, err := s.CreateUser(context.Background(), input)
require.NoError(t, err)
// Создаем пользователя с тем же email
_, err = s.CreateUser(context.Background(), input)
require.Error(t, err)
var appErr *errors.AppError
require.ErrorAs(t, err, &appErr)
assert.Equal(t, errors.ECONFLICT, appErr.Code)
assert.Contains(t, appErr.Message, "already exists")
}
Эволюция: метрики и мониторинг
Любая система вылезающих ошибок требует мониторинга:
# Prometheus метрики
errors_total{operation="CreateUser", type="E_INVALID"} 5
errors_total{operation="ProcessPayment", type="E_INTERNAL"} 1
Отправляйте алерты на разные уровни ошибок через Grafana/Sentry.
Заключение
Проработка обработки ошибок требует структурирования по трём уровням:
- Детальные, контекстные ошибки в бизнес-логике
- Преобразование в клиентские ответы в транспорте
- Долгосрочный мониторинг всех сбоев
Код с "просто ерроррейзер" превращается в систему, когда:
- Ошибки классифицированы
- Содержат операционный контекст
- Сообщения клиенту и логи разделены
- Автоматически собираются метрики
- Тесты покрывают экстремальные случаи
Стоимость отказа от хорошей обработки ошибок — часы отладки и уязвимости безопасности. Цена её внедрения — пара часов архитекторских усилий.
Выигрыш: детерминировано обрабатываем любые проблемы в один клик, а не гуглим лог-файлы. Пользователи получают полезные сообщения, разработчики — содержательные трассировки, продакшн — стабильное ночное время.