← Назад к вопросам

Как обработать ошибку при изменении данных?

1.3 Junior🔥 111 комментариев
#Soft Skills и рабочие процессы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Стратегия обработки ошибок при обновлении

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

Реализация с React Hooks и асинхронными операциями

import { useState } from 'react';
import { useCallback } from 'react';

interface UpdateUserPayload {
  name: string;
  email: string;
}

interface ApiError {
  status: number;
  message: string;
  field?: string;
}

export function UserProfileEditor() {
  const [data, setData] = useState({ name: 'John', email: 'john@example.com' });
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [isLoading, setIsLoading] = useState(false);
  const [successMessage, setSuccessMessage] = useState('');
  const [originalData, setOriginalData] = useState(data);

  const validateData = (payload: UpdateUserPayload): boolean => {
    const newErrors: Record<string, string> = {};

    if (!payload.name.trim()) {
      newErrors.name = 'Имя не может быть пустым';
    }
    if (!payload.email.includes('@')) {
      newErrors.email = 'Некорректный формат email';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleUpdate = useCallback(async (payload: UpdateUserPayload) => {
    setErrors({});
    setSuccessMessage('');

    if (!validateData(payload)) {
      return;
    }

    setIsLoading(true);

    try {
      const response = await fetch('/api/v1/user/profile', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        const errorData: ApiError = await response.json();

        if (response.status === 400) {
          setErrors({
            [errorData.field || 'general']: errorData.message
          });
        } else if (response.status === 409) {
          setErrors({ email: 'Этот email уже зарегистрирован' });
        } else if (response.status === 401) {
          window.location.href = '/login';
          return;
        } else if (response.status === 500) {
          setErrors({ general: 'Ошибка сервера. Попробуйте позже.' });
        }

        setData(originalData);
        return;
      }

      const updatedData = await response.json();
      setData(updatedData);
      setOriginalData(updatedData);
      setSuccessMessage('Данные успешно обновлены');
      
      setTimeout(() => setSuccessMessage(''), 3000);
    } catch (error) {
      if (error instanceof Error) {
        setErrors({ general: `Сетевая ошибка: ${error.message}` });
      } else {
        setErrors({ general: 'Неизвестная ошибка' });
      }
      setData(originalData);
    } finally {
      setIsLoading(false);
    }
  }, [originalData]);

  const handleChange = (field: string, value: string) => {
    setData(prev => ({ ...prev, [field]: value }));
    setErrors(prev => ({ ...prev, [field]: '' }));
  };

  const handleCancel = () => {
    setData(originalData);
    setErrors({});
  };

  return (
    <form onSubmit={(e) => { e.preventDefault(); handleUpdate(data); }}>
      {errors.general && (
        <div className="p-3 bg-red-100 text-red-700 rounded-lg mb-4">
          {errors.general}
        </div>
      )}

      {successMessage && (
        <div className="p-3 bg-green-100 text-green-700 rounded-lg mb-4">
          {successMessage}
        </div>
      )}

      <div className="mb-4">
        <label className="block text-sm font-medium mb-1">Имя</label>
        <input
          type="text"
          value={data.name}
          onChange={(e) => handleChange('name', e.target.value)}
          className={`w-full px-3 py-2 border rounded ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
          disabled={isLoading}
        />
        {errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
      </div>

      <div className="mb-4">
        <label className="block text-sm font-medium mb-1">Email</label>
        <input
          type="email"
          value={data.email}
          onChange={(e) => handleChange('email', e.target.value)}
          className={`w-full px-3 py-2 border rounded ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
          disabled={isLoading}
        />
        {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
      </div>

      <div className="flex gap-2">
        <button
          type="submit"
          disabled={isLoading}
          className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
        >
          {isLoading ? 'Сохранение...' : 'Сохранить'}
        </button>
        <button
          type="button"
          onClick={handleCancel}
          disabled={isLoading}
          className="px-4 py-2 border rounded hover:bg-gray-100 disabled:opacity-50"
        >
          Отменить
        </button>
      </div>
    </form>
  );
}

Типы ошибок и их обработка

Ошибки валидации (400): Показываем ошибки рядом с полями, чтобы пользователь сразу понял, что нужно исправить.

Конфликты данных (409): Возникают, когда данные конфликтуют с существующими (например, email занят). Откатываем состояние и показываем конкретное сообщение.

Сетевые ошибки: Обрабатываем в catch блоке. Показываем пользователю, что это техническая проблема, а не его ошибка.

Сессия истекла (401): Перенаправляем на логин вместо показания сообщения об ошибке.

Best Practices

Откат состояния: Всегда сохраняем исходные данные в originalData, чтобы откатить изменения при ошибке.

UX обратной связи: Показываем статус (загрузка, успех, ошибка) через состояния. Кнопка заблокирована во время загрузки, чтобы избежать дублированных запросов.

Таймаут сообщений: Сообщение об успехе автоматически исчезает, чтобы не загромождать интерфейс.