Dexie.PrematureCommitError

Inheritance Hierarchy

Since 2.0.0

Description

This exception will be thrown when the indexedDB transaction commits before the promise returned by your transaction scope is resolved or rejected.

Solution:

  1. Make sure to never call other async APIs from within a transaction
  2. Always use the global Promise (or Dexie.Promise) inside transactions.
  3. If using native async/await, avoid awaiting non-promises in loops.

NOT OK:

db.transaction ('rw', db.friends, () => {
    return fetch(someUrl); // fetch() is a non-indexedDB async API.
});

db.transaction ('rw', db.friends, () => {
    return new Promise (resolve => {
        setTimeout(resolve, 100); // setTimeout is non-indexedDB async API.
    });
});

db.transaction ('rw', db.friends, async () => {
    for (let i=0; i<100; ++i) {
        await null; // Don't await non-promises.
    }
});
OK:
db.transaction ('rw', db.friends, async () => {
    for (let i=0; i<100; ++i) {
        // If you need to await something that may be non-promise,
        // Use Promise.resolve() around it.
        await Promise.resolve(null); 
    }
});

Dont call setTimeout() or any other async API from inside a transaction.

NOT OK:

let Promise = require('bluebird');
db.transaction('r', db.friends, () => {
    // Promise is not the global Promise. It might break transactions.
    return new Promise((resolve, reject) => {
        db.friends.get(1).then(resolve, reject);
    });
});

Don’t use 3-rd party promises within transactions. Must only use Dexie.Promise or the built-in promise (window.Promise / self.Promise / global.Promise) within transactions.

THIS IS OK (in Dexie 2.0.0 and above):

db.transaction('r', db.friends, function () {
    // In Dexie 2.0, it's ok to use the global Promise (window.Promise)
    return Promise.all([
        db.friends.get(1),
        db.friends.get(2)
    ]);
});

THIS IS ALSO OK:

db.transaction('r', db.friends, async () => {
    // In Dexie 2.0, it's ok to use the global Promise (window.Promise)
    return await Promise.all([
        db.friends.get(1),
        db.friends.get(2),
        Promise.resolve(3) // Encapsulate non-promises with Promise.resolve() when awaiting things.
    ]);
});

Since Dexie 2.0, you may use the global Promise within transactions, since it will always be temporary patched within the transaction zone. But interactions with non-Dexie API:s must only be done outside transactions. For example if you need to fetch data from a REST API, do that before entering the transaction. And if you need to call REST API based on a database query, do that when your transaction completes.

THIS IS OK:

async function sample() {

    // First, you fetch:
    let restData = await fetch(someUrl);

    // Then, you do the transaction:
    let dbResult = await db.transaction('rw', db.someTable, async ()=> {
        let changedItems = await db.someTable
            .where('changeDate').above(lastSyncDate)
            .toArray();

        await db.someTable.bulkPut(restData);

        return changedItems;
    });

    // When transaction has finally completed, you can interact with fetch again:
    await fetch(someUrl, {method: 'POST', body: dbResult});
}

In this trivial sync-sample, the fetch() API is only called from outside the transaction. The sample applies also to XMLHttpRequest, jQuery Ajax and any other asynchronic API except the Dexie API.

Dexie.waitFor()

You can also keep a transaction alive using Dexie.waitFor() within a transaction, but I would only recommend it when using definitive short-lived operations, such as the crypto API - and avoid using it on ajax calls as they may take long time to complete - and your transaction would be locked for a long time if so.

Parallel transactions

It’s also OK to run several different database transactions in parallel. Transactions are maintained using zones.

THIS IS OK:

let [drdree, snoopy] = await Promise.all([

    db.transaction('r', db.friends, db.friendAddresses, async () => {
        let friend = await db.friends.get("drdree");
        friend.addresses = await db.friendAddresses
            .where('friendId').equals(friend.id)
            .toArray();

        return friend;
    }),

    db.transaction('r', db.pets, db.friends, () => {
        return db.pets.get("snoopy").then (snoopy => {
            return db.friends.get(snoopy.ownerId).then(owner => {
                snoopy.owner = owner;
                return snoopy;
            });
        });
    })
]);

console.log(JSON.stringify(drdree));
console.log(JSON.stringify(snoopy));

The two transactions can run in parallel. Zones will make sure that each time someone uses a table, it will be invoked using that current transaction of that particular flow of async calls.

Table of Contents