Типы ошибок в Dexie

Система обработки ошибок в Dexie.js построена вокруг специализированной иерархии исключений, которая позволяет точно определять причины сбоев при работе с IndexedDB. В отличие от прямого использования IndexedDB API, где разработчик часто сталкивается с малоинформативными объектами ошибок браузера, Dexie предоставляет собственные классы исключений с более понятными названиями и предсказуемым поведением.

Большинство ошибок наследуются от базового класса Dexie.DexieError, который в свою очередь наследует стандартный объект Error.

Пример общей обработки ошибок:

try {
    await db.users.add({
        id: 1,
        name: "Alex"
    });
}
catch (error) {
    console.error(error.name);
    console.error(error.message);
}

Свойство name обычно содержит конкретный тип ошибки:

ConstraintError
DataError
TransactionInactiveError
VersionError

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


Базовый класс DexieError

Практически все специфические ошибки библиотеки наследуются от:

Dexie.DexieError

Структура объекта:

try {
    await db.users.add(user);
}
catch (error) {
    console.log(error.name);
    console.log(error.message);
    console.log(error.stack);
}

Основные свойства:

Свойство Назначение
name Имя ошибки
message Описание причины
stack Стек вызовов
inner Вложенная ошибка IndexedDB

Поле inner особенно полезно для глубокого анализа проблем браузерного движка.


ConstraintError

Назначение

Одна из самых распространённых ошибок.

Возникает при нарушении ограничения уникальности ключей или индексов.

Пример:

db.version(1).stores({
    users: "id,&email"
});

Уникальный индекс:

&email

Попытка добавить дубликат:

await db.users.add({
    id: 1,
    email: "admin@mail.com"
});

await db.users.add({
    id: 2,
    email: "admin@mail.com"
});

Результат:

ConstraintError

Обработка:

try {
    await db.users.add(user);
}
catch (error) {
    if (error.name === "ConstraintError") {
        console.log("Запись уже существует");
    }
}

Типичные причины

  • дублирование первичного ключа;
  • нарушение уникального индекса;
  • вставка уже существующего объекта;
  • конфликт при массовом импорте данных.

DataError

Назначение

Появляется при передаче недопустимых данных в IndexedDB.

Пример:

await db.users.get(NaN);

Или:

await db.users.where("id").equals(undefined).toArray();

Возможный результат:

DataError

Причины возникновения

  • использование undefined как ключа;
  • использование NaN;
  • некорректный диапазон поиска;
  • неподдерживаемый тип ключа.

Некорректно:

await db.users.delete(undefined);

Корректно:

if (id !== undefined) {
    await db.users.delete(id);
}

InvalidArgumentError

Назначение

Возникает при передаче недопустимых аргументов в методы Dexie.

Пример:

db.table(null);

Или:

db.transaction("rw");

Если параметры метода не соответствуют ожидаемому формату, Dexie может сгенерировать:

InvalidArgumentError

Причины

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

InvalidTableError

Назначение

Сообщает о попытке обращения к таблице, которой нет в схеме базы данных.

Пример:

db.table("orders");

Если таблица отсутствует:

db.version(1).stores({
    users: "++id,name"
});

возникнет:

InvalidTableError

Безопасный вариант:

if (db.tables.some(t => t.name === "orders")) {
    const table = db.table("orders");
}

NoSuchDatabaseError

Назначение

Возникает при попытке открыть или удалить несуществующую базу данных.

Пример:

await Dexie.delete("UnknownDatabase");

В зависимости от ситуации может появиться:

NoSuchDatabaseError

Подобные ошибки чаще встречаются в административных сценариях управления несколькими базами.


OpenFailedError

Назначение

Сообщает о невозможности открыть базу данных.

Пример:

const db = new Dexie("AppDB");

await db.open();

Причинами могут быть:

  • повреждение данных;
  • ошибки миграции;
  • конфликт версий;
  • блокировка браузером.

Результат:

OpenFailedError

Обработка:

try {
    await db.open();
}
catch (error) {
    if (error.name === "OpenFailedError") {
        console.error("Не удалось открыть базу");
    }
}

VersionError

Назначение

Связана с проблемами версионирования базы данных.

Пример:

db.version(2).stores({
    users: "++id,name"
});

Если браузер обнаруживает конфликт между версиями:

VersionError

Чаще всего подобные ошибки возникают при:

  • некорректных миграциях;
  • повреждении метаданных;
  • конфликте между вкладками.

UpgradeError

Назначение

Возникает во время обновления структуры базы данных.

Пример миграции:

db.version(2)
.stores({
    users: "++id,name,age"
})
.upgrade(tx => {
    return tx.table("users")
        .toCollection()
        .modify(user => {
            user.age = 18;
        });
});

Если внутри функции обновления возникает исключение:

throw new Error("Migration failed");

Dexie оборачивает его в:

UpgradeError

Особенности

Обычно содержит ссылку на внутреннюю причину:

catch(error) {
    console.log(error.inner);
}

DatabaseClosedError

Назначение

Появляется при обращении к закрытой базе данных.

Пример:

db.close();

await db.users.toArray();

Результат:

DatabaseClosedError

Безопасный подход:

if (!db.isOpen()) {
    await db.open();
}

TransactionInactiveError

Назначение

Возникает при использовании завершённой транзакции.

Пример:

await db.transaction("rw", db.users, async () => {

    setTimeout(async () => {
        await db.users.add({
            name: "John"
        });
    }, 1000);

});

После завершения транзакции её контекст исчезает.

Попытка работы внутри асинхронного обработчика приводит к:

TransactionInactiveError

Частая причина

Использование:

setTimeout()

или сторонних асинхронных API внутри транзакции.


AbortError

Назначение

Появляется после принудительного прерывания транзакции.

Пример:

await db.transaction("rw", db.users, async () => {

    Dexie.currentTransaction.abort();

});

Результат:

AbortError

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


TimeoutError

Назначение

Используется в механизмах, связанных с ожиданием асинхронных операций.

Возникает при превышении допустимого времени ожидания.

Пример логики:

await Promise.race([
    operation(),
    timeoutPromise()
]);

При срабатывании тайм-аута может быть выброшен:

TimeoutError

BulkError

Назначение

Специальная ошибка пакетных операций.

Методы:

bulkAdd()
bulkPut()
bulkDelete()

могут завершиться частично успешно.

Пример:

await db.users.bulkAdd([
    {id: 1},
    {id: 2},
    {id: 1}
]);

Результат:

BulkError

Особенность

Содержит информацию о проблемных элементах.

Пример:

catch(error) {

    console.log(error.failures);
    console.log(error.failuresByPos);

}

Структура позволяет определить, какие записи были добавлены успешно, а какие вызвали ошибку.


ModifyError

Назначение

Связана с массовым изменением записей.

Пример:

await db.users
    .toCollection()
    .modify(user => {

        if (!user.email) {
            throw new Error("Email required");
        }

        user.active = true;
    });

Результат:

ModifyError

Дополнительная информация

Ошибка содержит сведения о количестве успешных и неуспешных изменений.

Например:

catch(error) {

    console.log(error.failedKeys);
    console.log(error.successCount);

}

PrematureCommitError

Назначение

Появляется при преждевременном завершении транзакции.

Обычно связана со смешиванием различных асинхронных механизмов.

Пример потенциально проблемного кода:

await db.transaction("rw", db.users, async () => {

    fetch("/api/users")
        .then(response => response.json())
        .then(data => {
            return db.users.add(data);
        });

});

Транзакция может завершиться раньше сетевого запроса.

В результате возможно появление:

PrematureCommitError

MissingAPIError

Назначение

Сообщает об отсутствии IndexedDB в среде выполнения.

Встречается в:

  • устаревших браузерах;
  • некоторых тестовых окружениях;
  • серверном JavaScript.

Пример:

MissingAPIError

Для серверных приложений обычно используются специальные реализации IndexedDB либо мок-объекты.


Обработка нескольких типов ошибок

На практике редко ограничиваются одной проверкой.

Пример:

try {

    await db.users.add(user);

}
catch(error) {

    switch (error.name) {

        case "ConstraintError":
            console.log("Дубликат записи");
            break;

        case "DataError":
            console.log("Некорректные данные");
            break;

        case "DatabaseClosedError":
            console.log("База закрыта");
            break;

        default:
            console.error(error);
    }

}

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


Использование catch по имени ошибки

Dexie поддерживает фильтрацию исключений по типу.

Пример:

db.users
    .add(user)
    .catch("ConstraintError", error => {

        console.log("Пользователь уже существует");

    });

Несколько типов:

.catch(error => {

    console.error(error);

});

Подобный механизм делает код компактнее по сравнению с традиционными проверками через if и switch.


Стратегии проектирования устойчивой обработки ошибок

При разработке крупных приложений ошибки Dexie обычно разделяют на несколько категорий:

Ошибки данных

  • ConstraintError;
  • DataError;
  • ModifyError;
  • BulkError.

Ошибки схемы

  • InvalidTableError;
  • VersionError;
  • UpgradeError.

Ошибки жизненного цикла базы

  • OpenFailedError;
  • DatabaseClosedError;
  • NoSuchDatabaseError.

Ошибки транзакций

  • AbortError;
  • TransactionInactiveError;
  • PrematureCommitError.

Ошибки окружения

  • MissingAPIError;
  • TimeoutError.

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