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

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

Типовая схема работы с удалённым источником строится вокруг обновления свойства list экземпляра Awesomplete после завершения асинхронной операции:

const input = document.querySelector("#search");
const awesomplete = new Awesomplete(input, {
  minChars: 1,
  autoFirst: true,
  list: []
});

async function loadSuggestions(query) {
  const response = await fetch(`/api/suggest?q=${encodeURIComponent(query)}`);
  const data = await response.json();
  awesomplete.list = data;
}

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


Классы ошибок при загрузке данных

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

Сетевые сбои

К ним относятся:

  • отсутствие соединения
  • таймаут запроса
  • обрыв соединения во время передачи данных
try {
  const response = await fetch(url);
} catch (err) {
  // NetworkError, AbortError и другие низкоуровневые сбои
}

Ошибки HTTP-уровня

Даже при успешном соединении сервер может вернуть некорректный статус:

  • 400 — ошибка запроса
  • 404 — отсутствие ресурса
  • 500 — внутренняя ошибка сервера
if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}

Ошибки парсинга данных

Awesomplete ожидает массив строк или объектов определённой структуры. Несоответствие формата приводит к некорректному отображению или полному отказу списка.

const data = await response.json();

if (!Array.isArray(data)) {
  throw new Error("Invalid format: expected array");
}

Пустые или частично заполненные ответы

Сервер может вернуть пустой массив или элементы с отсутствующими полями, что приводит к «пустому автокомплиту» или визуальным артефактам.


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

Стандартная модель обработки строится через try/catch с обязательной очисткой или безопасным сбросом списка:

async function loadSuggestions(query) {
  try {
    const response = await fetch(`/api/suggest?q=${encodeURIComponent(query)}`);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();

    if (!Array.isArray(data)) {
      throw new Error("Invalid response format");
    }

    awesomplete.list = data;
  } catch (err) {
    awesomplete.list = [];
  }
}

Такая стратегия предотвращает отображение устаревших или повреждённых данных.


Состояние гонки и устаревшие ответы

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

Это приводит к эффекту «перезаписи списка устаревшими данными».

Решение строится через идентификатор запроса:

let requestId = 0;

async function loadSuggestions(query) {
  const currentId = ++requestId;

  try {
    const response = await fetch(`/api/suggest?q=${query}`);
    const data = await response.json();

    if (currentId !== requestId) {
      return;
    }

    awesomplete.list = data;
  } catch (err) {
    if (currentId === requestId) {
      awesomplete.list = [];
    }
  }
}

Такой подход гарантирует, что только последний запрос влияет на интерфейс.


AbortController и отмена запросов

Более современный способ управления устаревшими запросами — использование AbortController.

let controller = null;

async function loadSuggestions(query) {
  if (controller) {
    controller.abort();
  }

  controller = new AbortController();

  try {
    const response = await fetch(`/api/suggest?q=${query}`, {
      signal: controller.signal
    });

    const data = await response.json();
    awesomplete.list = data;
  } catch (err) {
    if (err.name === "AbortError") {
      return;
    }

    awesomplete.list = [];
  }
}

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


Обработка таймаутов

Fetch не имеет встроенного таймаута, поэтому его моделируют через Promise.race.

function fetchWithTimeout(url, timeout = 3000) {
  const controller = new AbortController();

  const timer = setTimeout(() => controller.abort(), timeout);

  return fetch(url, { signal: controller.signal })
    .finally(() => clearTimeout(timer));
}

Интеграция с Awesomplete:

async function loadSuggestions(query) {
  try {
    const response = await fetchWithTimeout(`/api/suggest?q=${query}`);
    const data = await response.json();

    awesomplete.list = Array.isArray(data) ? data : [];
  } catch (err) {
    awesomplete.list = [];
  }
}

Fallback-стратегии

При недоступности API важно сохранять функциональность интерфейса. Используются резервные источники:

Локальный статический список

const fallbackList = ["apple", "banana", "orange", "grape"];

function setFallback() {
  awesomplete.list = fallbackList;
}

Кеширование предыдущих успешных запросов

const cache = new Map();

async function loadSuggestions(query) {
  if (cache.has(query)) {
    awesomplete.list = cache.get(query);
    return;
  }

  try {
    const response = await fetch(`/api/suggest?q=${query}`);
    const data = await response.json();

    cache.set(query, data);
    awesomplete.list = data;
  } catch (err) {
    awesomplete.list = cache.get(query) || [];
  }
}

Обработка ошибок формата данных

Даже при успешной доставке данных важно защищаться от некорректной структуры:

function normalizeData(data) {
  if (!Array.isArray(data)) return [];

  return data
    .filter(item => typeof item === "string" && item.length > 0)
    .slice(0, 20);
}

Интеграция:

const data = await response.json();
awesomplete.list = normalizeData(data);

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

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

function logError(context, error) {
  console.error(`[Awesomplete:${context}]`, error);
}

Применение:

catch (err) {
  logError("loadSuggestions", err);
  awesomplete.list = [];
}

В продакшн-средах вместо console.error обычно используется отправка в сервисы мониторинга.


Защита от «пустого UI»

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

function handleErrorState() {
  awesomplete.list = [];
  input.setAttribute("data-error", "true");
}

Сброс состояния:

function clearErrorState() {
  input.removeAttribute("data-error");
}

Комбинированная стратегия устойчивой загрузки

На практике используется комбинация нескольких подходов:

  • отмена устаревших запросов через AbortController
  • кеширование успешных ответов
  • проверка структуры данных
  • fallback-списки
  • защита от гонок запросов
  • таймауты
let controller = null;
const cache = new Map();

async function loadSuggestions(query) {
  if (cache.has(query)) {
    awesomplete.list = cache.get(query);
    return;
  }

  if (controller) controller.abort();
  controller = new AbortController();

  try {
    const response = await fetchWithTimeout(
      `/api/suggest?q=${query}`,
      3000,
      controller.signal
    );

    const data = await response.json();
    const normalized = normalizeData(data);

    cache.set(query, normalized);
    awesomplete.list = normalized;

  } catch (err) {
    awesomplete.list = cache.get(query) || [];
  }
}

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