Dexie.js строится поверх IndexedDB и наследует его транзакционную
модель, где операции выполняются атомарно внутри транзакций. Любая
ошибка внутри транзакции приводит к её автоматическому откату, однако
поведение зависит от того, как именно ошибка была вызвана: выброс
исключения, отклонение Promise или явный вызов abort().
В основе обработки ошибок лежит принцип: транзакция считается успешной только если все операции внутри неё завершились без исключений и отклонений. Любая ошибка переводит транзакцию в состояние abort, после чего IndexedDB гарантирует отмену всех изменений, сделанных в рамках этого блока.
Dexie различает несколько типов ошибок, влияющих на выполнение транзакции:
throw new Error)transaction.abort()Каждый из этих случаев приводит к разному моменту остановки выполнения, но итоговый результат одинаков: транзакция помечается как отменённая и изменения откатываются.
Любое исключение, возникшее внутри функции транзакции, автоматически приводит к её прерыванию:
db.transaction('rw', db.users, async () => {
await db.users.add({ id: 1, name: 'Alex' });
throw new Error('Ошибка бизнес-логики');
await db.users.add({ id: 2, name: 'Bob' });
});
После выброса ошибки:
Особенность IndexedDB заключается в том, что откат происходит не постфактум, а на уровне движка базы данных, что обеспечивает целостность данных.
Асинхронные операции внутри транзакции также управляются через Promise. Если любой Promise в цепочке отклоняется, транзакция считается проваленной:
db.transaction('rw', db.orders, async () => {
await db.orders.add({ id: 1 });
await Promise.reject(new Error('Сбой операции'));
await db.orders.add({ id: 2 });
});
Dexie автоматически перехватывает rejection и переводит транзакцию в aborted state.
Важно учитывать, что даже “пойманная” ошибка может привести к откату, если она была выброшена до обработки:
db.transaction('rw', db.orders, async () => {
try {
await Promise.reject(new Error('Ошибка'));
} catch (e) {
// обработка не предотвращает откат транзакции
}
});
В данном случае транзакция всё равно считается неуспешной, поскольку rejection уже произошёл внутри контекста транзакции.
Dexie предоставляет механизм ручного прерывания транзакции через
transaction.abort().
db.transaction('rw', db.users, async (tx) => {
const user = await db.users.get(1);
if (!user) {
tx.abort();
return;
}
await db.users.update(1, { active: true });
});
После вызова abort():
abort() особенно полезен в сценариях бизнес-валидации,
когда ошибка не является исключением, но выполнение должно быть
остановлено.
IndexedDB может генерировать системные ошибки, которые Dexie не преобразует в бизнес-исключения:
ConstraintError)Пример:
db.transaction('rw', db.users, async () => {
await db.users.add({ id: 1 });
await db.users.add({ id: 1 }); // ConstraintError
});
Такие ошибки автоматически приводят к rollback без необходимости ручного вмешательства.
try/catch внутри транзакции позволяет перехватывать ошибки, но не всегда предотвращает откат:
db.transaction('rw', db.users, async () => {
try {
await db.users.add({ id: 1 });
throw new Error('fail');
} catch (e) {
console.log('перехвачено');
}
});
Несмотря на перехват, транзакция будет считаться неуспешной, если ошибка не “поглощена” корректным образом до завершения Promise цепочки.
Ключевой момент: Dexie ориентируется не на факт обработки ошибки, а на состояние Promise транзакции.
Dexie обеспечивает атомарность операций:
Это означает, что при любом сбое состояние базы остаётся консистентным.
Особенно важно учитывать это при работе с несколькими таблицами:
db.transaction('rw', db.users, db.logs, async () => {
await db.users.add({ id: 1 });
await db.logs.add({ event: 'create user' });
throw new Error('fail');
});
Обе операции будут отменены, даже если одна из них уже была выполнена.
Dexie поддерживает вложенные транзакции, но они не создают независимых rollback-секций. Ошибка во внутреннем блоке приводит к откату всей внешней транзакции:
db.transaction('rw', db.users, async () => {
await db.users.add({ id: 1 });
await db.transaction('rw', db.users, async () => {
throw new Error('inner fail');
});
});
Результат:
Dexie позволяет подписываться на события транзакции:
db.transaction('rw', db.users, async (tx) => {
tx.on('error', (err) => {
console.log('ошибка транзакции:', err);
});
await db.users.add({ id: 1 });
throw new Error('fail');
});
Доступные события:
error — ошибка до откатаcomplete — успешное завершениеabort — транзакция отмененаЭти события полезны для логирования и мониторинга состояния базы.
После отката транзакции Dexie не выполняет автоматический retry. Повторная попытка должна быть реализована вручную:
async function safeWrite() {
try {
await db.transaction('rw', db.users, async () => {
await db.users.add({ id: 1 });
throw new Error('fail');
});
} catch (e) {
return safeWrite();
}
}
При этом важно учитывать риск бесконечных циклов при постоянных ошибках.
Dexie использует внутреннюю интеграцию с async context транзакции. Это означает:
db.transaction('rw', db.users, async () => {
setTimeout(async () => {
await db.users.add({ id: 1 });
}, 100);
});
В этом случае операция может выполниться вне транзакции и не будет откатана.
Поведение Dexie позволяет строить несколько стратегий:
tx.on('error')Ключевым элементом остаётся понимание того, что транзакция — это неделимая единица выполнения, и любая ошибка внутри неё разрушает весь блок операций.