Обработка ошибок загрузки

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

Slim Select поддерживает загрузку опций через ajax-подобные конфигурации или пользовательские функции получения данных. В этих сценариях обработка ошибок распределяется по нескольким уровням:

  • уровень сетевого запроса (fetch, XMLHttpRequest);
  • уровень обработки ответа (парсинг JSON, трансформация данных);
  • уровень интеграции с состоянием компонента (обновление списка опций);
  • уровень пользовательского взаимодействия (поиск, ввод, открытие dropdown).

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

Типичный сценарий загрузки:

new SlimSelect({
  select: '#select',
  ajax: (search, callback) => {
    fetch(`/api/items?q=${search}`)
      .then(res => res.json())
      .then(data => {
        callback(data.items);
      });
  }
});

В данной конструкции отсутствует обработка ошибок, что делает компонент уязвимым к следующим ситуациям:

  • сетевой сбой (timeout, offline);
  • ответ сервера с кодом 500;
  • некорректный JSON;
  • отсутствие ожидаемого поля items.

Базовая стратегия перехвата ошибок

Основной механизм защиты строится вокруг расширения цепочки промисов блоком catch. Любая ошибка должна приводить к безопасному завершению запроса без вызова callback с некорректными данными.

ajax: (search, callback) => {
  fetch(`/api/items?q=${search}`)
    .then(res => {
      if (!res.ok) {
        throw new Error('HTTP error');
      }
      return res.json();
    })
    .then(data => {
      if (!data || !Array.isArray(data.items)) {
        throw new Error('Invalid response structure');
      }
      callback(data.items);
    })
    .catch(err => {
      console.error('SlimSelect load error:', err);
      callback([]);
    });
}

Ключевой принцип заключается в том, что callback вызывается всегда, даже при ошибке, но с пустым массивом. Это предотвращает зависание интерфейса в состоянии загрузки.

Изоляция ошибок парсинга данных

Частая проблема возникает не на уровне сети, а на этапе преобразования данных. API может возвращать валидный JSON, но структура может отличаться от ожидаемой.

Пример потенциально опасного преобразования:

data.items.map(i => ({
  text: i.name,
  value: i.id
}));

Если data.items не является массивом, выполнение приведёт к исключению TypeError. Для предотвращения подобных ситуаций используется явная нормализация:

.then(data => {
  const items = Array.isArray(data.items) ? data.items : [];

  const safeItems = items
    .filter(i => i && typeof i === 'object')
    .map(i => ({
      text: i.name ?? 'undefined',
      value: i.id ?? ''
    }));

  callback(safeItems);
})

Такой подход гарантирует, что даже частично повреждённые данные не нарушат работу компонента.

Обработка ошибок таймаута и прерывания запросов

При активном вводе пользователя Slim Select может инициировать серию запросов. Это создаёт риск гонки состояний: более старый запрос может завершиться позже нового и перезаписать актуальные данные.

Решение включает использование AbortController:

let controller = null;

ajax: (search, callback) => {
  if (controller) {
    controller.abort();
  }

  controller = new AbortController();

  fetch(`/api/items?q=${search}`, {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(data => callback(data.items))
    .catch(err => {
      if (err.name === 'AbortError') return;
      console.error(err);
      callback([]);
    });
}

AbortError специально игнорируется, так как он не является критической ошибкой, а отражает нормальное поведение отмены предыдущего запроса.

Централизованная обработка ошибок загрузки

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

function safeFetch(url) {
  return fetch(url)
    .then(res => {
      if (!res.ok) throw new Error(res.statusText);
      return res.json();
    })
    .catch(err => {
      console.warn('Fetch failed:', err);
      return null;
    });
}

Интеграция:

ajax: (search, callback) => {
  safeFetch(`/api/items?q=${search}`)
    .then(data => {
      if (!data) return callback([]);
      callback(data.items || []);
    });
}

Такой слой абстракции снижает количество точек отказа и упрощает тестирование.

Обработка ошибок при инициализации компонента

Ошибки могут возникать не только при загрузке данных, но и при создании экземпляра Slim Select. Неверный селектор, отсутствие DOM-элемента или повторная инициализация приводят к исключениям.

try {
  new SlimSelect({
    select: '#select',
    ajax: (search, callback) => {
      fetch(`/api/items?q=${search}`)
        .then(res => res.json())
        .then(data => callback(data.items))
        .catch(() => callback([]));
    }
  });
} catch (e) {
  console.error('SlimSelect init failed:', e);
}

Дополнительной защитой служит проверка существования элемента до инициализации:

const el = document.querySelector('#select');
if (!el) {
  console.warn('Select element not found');
} else {
  new SlimSelect({ select: el });
}

Поведение интерфейса при ошибках

С точки зрения UX, обработка ошибок загрузки должна исключать следующие состояния:

  • бесконечный индикатор загрузки;
  • пустой dropdown без пояснений при активном запросе;
  • блокировка ввода пользователя;
  • отсутствие реакции на повторный поиск.

Практика безопасного поведения включает:

  • возврат пустого массива при сбое;
  • сброс состояния загрузки после catch;
  • сохранение предыдущих валидных данных при повторной ошибке.
let lastValidItems = [];

ajax: (search, callback) => {
  fetch(`/api/items?q=${search}`)
    .then(res => res.json())
    .then(data => {
      lastValidItems = data.items || [];
      callback(lastValidItems);
    })
    .catch(() => {
      callback(lastValidItems);
    });
}

Такой подход позволяет сохранять функциональность даже при нестабильном соединении.

Логирование и диагностика ошибок

Встроенное логирование ошибок загрузки позволяет отслеживать деградацию API и поведение пользователей. Логирование не должно нарушать поток выполнения.

.catch(err => {
  queueMicrotask(() => {
    console.error('SlimSelect AJAX error:', {
      message: err.message,
      time: Date.now()
    });
  });
  callback([]);
});

Использование queueMicrotask или setTimeout минимизирует влияние логирования на основной поток UI.

Стратегии деградации функциональности

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

const cache = new Map();

ajax: (search, callback) => {
  if (cache.has(search)) {
    return callback(cache.get(search));
  }

  fetch(`/api/items?q=${search}`)
    .then(res => res.json())
    .then(data => {
      cache.set(search, data.items || []);
      callback(data.items || []);
    })
    .catch(() => {
      callback(cache.get(search) || []);
    });
}

Такой механизм снижает зависимость от сети и стабилизирует поведение интерфейса при повторяющихся запросах.

Контроль неконсистентного состояния

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

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