При работе с динамической загрузкой данных в Choices.js ошибки возникают как на стороне клиента, так и на стороне сервера. Наиболее распространённые проблемы связаны с сетью, форматом ответа API, авторизацией и обработкой асинхронных операций.
Типичные категории ошибок:
404, 500,
403);Без корректной обработки подобных ситуаций компонент выбора начинает вести себя нестабильно: список не обновляется, интерфейс зависает, появляются дубликаты элементов или полностью пропадает возможность выбора.
Чаще всего Choices.js используется совместно с fetch()
для получения данных с сервера.
Пример стандартной загрузки:
const element = document.querySelector('#users');
const choices = new Choices(element, {
searchEnabled: true,
shouldSort: false
});
async function loadUsers() {
const response = await fetch('/api/users');
const data = await response.json();
choices.setChoices(
data,
'value',
'label',
true
);
}
loadUsers();
В подобной реализации отсутствует защита от ошибок. Любая проблема приведёт к необработанному исключению.
Главный механизм защиты асинхронного кода — конструкция
try/catch.
async function loadUsers() {
try {
const response = await fetch('/api/users');
const data = await response.json();
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
console.error(error);
}
}
Теперь ошибки не прерывают выполнение приложения полностью.
fetch() не выбрасывает исключение при статусах
404 или 500. Необходимо вручную проверять
свойство response.ok.
Неправильный вариант:
const response = await fetch('/api/users');
const data = await response.json();
Правильный вариант:
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
const data = await response.json();
Это особенно важно при интеграции с REST API.
Ошибка не должна оставаться только в консоли браузера. Пользовательский интерфейс обязан сообщать о проблеме.
<div id="error"></div>
const errorBlock = document.querySelector('#error');
async function loadUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error('Ошибка загрузки');
}
const data = await response.json();
choices.setChoices(
data,
'value',
'label',
true
);
errorBlock.textContent = '';
} catch (error) {
errorBlock.textContent = error.message;
}
}
Даже при статусе 200 сервер может вернуть повреждённый
JSON.
Пример проблемного ответа:
{
"users": [
Вызов response.json() завершится ошибкой парсинга.
Безопасный вариант:
try {
const response = await fetch('/api/users');
const text = await response.text();
const data = JSON.parse(text);
} catch (error) {
console.error('Ошибка JSON:', error);
}
Подобный подход полезен при работе с нестабильными API.
Choices.js ожидает определённый формат данных. Если API возвращает неправильную структуру, компонент не сможет отобразить элементы.
Ожидаемый формат:
[
{
value: 1,
label: 'Admin'
}
]
Проверка структуры:
if (!Array.isArray(data)) {
throw new Error('Ответ должен быть массивом');
}
Проверка элементов:
data.forEach(item => {
if (!('value' in item) || !('label' in item)) {
throw new Error('Некорректная структура элемента');
}
});
API может вернуть пустой массив.
[]
Choices.js не выдаёт ошибку в такой ситуации, однако пользователь увидит пустой список без объяснений.
Решение:
if (data.length === 0) {
errorBlock.textContent = 'Данные отсутствуют';
return;
}
fetch() не поддерживает таймауты автоматически. При
медленном сервере интерфейс может зависнуть на неопределённое время.
async function loadUsers() {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 5000);
try {
const response = await fetch('/api/users', {
signal: controller.signal
});
clearTimeout(timeout);
const data = await response.json();
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
if (error.name === 'AbortError') {
console.error('Превышено время ожидания');
}
}
}
Сетевые ошибки могут быть временными. В подобных случаях используется механизм повторных запросов.
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Ошибка сервера');
}
return await response.json();
} catch (error) {
if (i === retries - 1) {
throw error;
}
}
}
}
Использование:
try {
const data = await fetchWithRetry('/api/users');
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
console.error(error);
}
При динамическом поиске пользователь может быстро вводить текст, создавая множество одновременных запросов.
Проблемы:
let controller;
async function searchUsers(query) {
if (controller) {
controller.abort();
}
controller = new AbortController();
try {
const response = await fetch(
`/api/users?q=${query}`,
{
signal: controller.signal
}
);
const data = await response.json();
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
}
Choices.js часто используется вместе с удалённым поиском.
Пример интеграции:
element.addEventListener('search', async (event) => {
try {
const response = await fetch(
`/api/search?q=${event.detail.value}`
);
if (!response.ok) {
throw new Error('Ошибка поиска');
}
const data = await response.json();
choices.clearChoices();
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
console.error(error);
}
});
Отсутствие визуального индикатора создаёт ощущение зависания интерфейса.
let isLoading = false;
async function loadUsers() {
if (isLoading) {
return;
}
isLoading = true;
try {
const response = await fetch('/api/users');
const data = await response.json();
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
console.error(error);
} finally {
isLoading = false;
}
}
<div id="loader">Загрузка...</div>
const loader = document.querySelector('#loader');
async function loadUsers() {
loader.style.display = 'block';
try {
const response = await fetch('/api/users');
const data = await response.json();
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
console.error(error);
} finally {
loader.style.display = 'none';
}
}
При неудачной загрузке старые данные могут остаться в компоненте и вводить пользователя в заблуждение.
catch (error) {
choices.clearChoices();
errorBlock.textContent = 'Ошибка обновления данных';
}
Многие API требуют токен доступа.
Пример:
const response = await fetch('/api/users', {
headers: {
Authorization: `Bearer ${token}`
}
});
Обработка 401 Unauthorized:
if (response.status === 401) {
throw new Error('Требуется авторизация');
}
Обработка 403 Forbidden:
if (response.status === 403) {
throw new Error('Доступ запрещён');
}
Если сервер не разрешает междоменные запросы, браузер блокирует запрос ещё до получения ответа.
Типичная ошибка:
Access to fetch has been blocked by CORS policy
На стороне клиента исправить проблему невозможно. Необходима настройка серверных заголовков:
Access-Control-Allow-Origin: *
Или:
Access-Control-Allow-Origin: https://example.com
Для диагностики необходимо фиксировать подробности ошибок.
catch (error) {
console.error({
message: error.message,
stack: error.stack,
time: new Date().toISOString()
});
}
При большом количестве компонентов удобнее вынести загрузку в универсальную функцию.
async function loadChoicesData(url, choicesInstance) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Ошибка ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('Неверный формат данных');
}
choicesInstance.clearChoices();
choicesInstance.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
choicesInstance.clearChoices();
console.error(error);
}
}
Использование:
loadChoicesData('/api/users', choices);
При временных ошибках можно использовать последние успешно загруженные данные.
let cachedData = [];
async function loadUsers() {
try {
const response = await fetch('/api/users');
const data = await response.json();
cachedData = data;
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
choices.setChoices(
cachedData,
'value',
'label',
true
);
}
}
Чрезмерное количество запросов увеличивает вероятность ошибок.
function debounce(callback, delay = 300) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
callback(...args);
}, delay);
};
}
Использование:
const debouncedSearch = debounce(searchUsers, 500);
element.addEventListener('search', event => {
debouncedSearch(event.detail.value);
});
Некоторые серверы возвращают ошибки внутри JSON.
Пример:
{
"success": false,
"message": "Limit exceeded"
}
Проверка:
if (data.success === false) {
throw new Error(data.message);
}
Иногда API возвращает null или неполные записи.
Безопасная фильтрация:
const normalized = data
.filter(item => item)
.filter(item => item.id && item.name)
.map(item => ({
value: item.id,
label: item.name
}));
Браузер позволяет определить потерю сети.
window.addEventListener('offline', () => {
console.error('Интернет-соединение потеряно');
});
Возврат соединения:
window.addEventListener('online', () => {
loadUsers();
});
Если API недоступен, можно переключиться на локальный набор данных.
const fallbackData = [
{
value: 1,
label: 'Guest'
}
];
Использование:
catch (error) {
choices.setChoices(
fallbackData,
'value',
'label',
true
);
}
const element = document.querySelector('#users');
const choices = new Choices(element);
const errorBlock = document.querySelector('#error');
let controller;
async function loadUsers(query = '') {
if (controller) {
controller.abort();
}
controller = new AbortController();
errorBlock.textContent = '';
try {
const response = await fetch(
`/api/users?q=${query}`,
{
signal: controller.signal
}
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('Некорректный формат');
}
choices.clearChoices();
choices.setChoices(
data,
'value',
'label',
true
);
} catch (error) {
if (error.name === 'AbortError') {
return;
}
choices.clearChoices();
errorBlock.textContent = error.message;
console.error(error);
}
}
element.addEventListener('search', event => {
loadUsers(event.detail.value);
});