1

I have a function that is used to add a record to the IndexDb database:

async function addAsync(storeName, object) {
    return new Promise((res, rej) => {
        // openDatabaseAsync() is another reusable method to open the db.  That works fine.
        openDatabaseAsync().then(db => {
            var store = openObjectStore(db, storeName, 'readwrite');
            var addResult = store.add(JSON.parse(object));
            addResult.onsuccess = res;
            addResult.onerror = (e) => {
                console.log("addResult Error");
                throw e;
            };
        }).catch(e => {
            // Error from "throw e;" above NOT GETTING CAUGHT HERE!
            console.error("addAsync ERROR > ", e, storeName, object);
            rej(e);
        });
    })
}

If I try to add a duplicate key, then I expect:

addResult.onerror = (e) => {
    console.log("addResult Error");
    throw e;
}

to capture that. It does.

But then, I also expect my

.catch(e => {
    // Error from "throw e;" above NOT GETTING CAUGHT HERE!
    console.error("addAsync ERROR > ", e, storeName, object);
    rej(e);
})

to catch that error. But instead I get an "uncaught" log.

Console output:

addResult Error
Uncaught Event {isTrusted: true, type: "error", target: IDBRequest, currentTarget: IDBRequest, eventPhase: 2, …}

Does that final .catch only handle exceptions from the openDatabaseAsync call? I would have thought now as it is chained to the .then.

In summary, here's what I would expect from the above code:

  • If openDatabaseAsync() fails then I'm not catching that so the error would be sent to the caller of addAsync().
  • If .then fails then I expect the .catch to catch it, log the error and then reject the promise meaning that the called of addAsync() would need to handle that.

However, I would have thought that I should get the log from the line:

console.error("addAsync ERROR > ", e, storeName, object);

before the reject is sent back to the caller of addAsync(), which may be unhandled at that point.

2
  • The thing that's uncaught would be the addAsync().catch() thing, since you're rejecting in the promise that gets returned from addAsync(). Do you have a catch on the result from that call? Commented Mar 1, 2021 at 13:25
  • THanks @TKoL. The answer to your question is "No". It's called by Blazor JSInterop but that's something I can move on to. But, to your point, I don't get the log "addAsync ERROR > ..." so that tells me that the throw e; is not getting caught by the .catch, which is where I'm confused at the moment. I can move on to external handling from the caller of addAsync() subsequently. Commented Mar 1, 2021 at 13:29

1 Answer 1

3

Your approach would benefit form a larger overhaul.

  • Generally, don't write a function as async when it's not also using await.
  • Don't use new Promise() for an operation that returns a promise, such as openDatabaseAsync() does. Return that promise, or switch to async/await.
  • It would be useful to wrap IndexedDB operations so that they follow promise semantics.

On the example of IDBRequest:

function promisifyIDBRequest(idbr) {
    return new Promise( (resolve, reject) => {
        idbr.onsuccess = () => resolve(idbr.result);
        idbr.onerror = (e) => reject(e.target.error);
    });
}

Now you can do this:

async function addAsync(storeName, object) {
    const db = await openDatabaseAsync();
    const store = openObjectStore(db, storeName, 'readwrite');
    return promisifyIDBRequest(store.add(JSON.parse(object)));
}

Add a try/catch block if you want to handle errors inside of addAsync().

It's worth checking out existing solutions that wrap the entire IndexedDB interface with promise semantics, such as https://github.com/jakearchibald/idb.


FWIW, the promise-chain variant of the above function would look like this:

function addAsync(storeName, object) {
    return openDatabaseAsync().then( (db) => {
        const store = openObjectStore(db, storeName, 'readwrite');
        return promisifyIDBRequest(store.add(JSON.parse(object)));
    });
}

both variants return a promise for an IDBRequest result.

Sign up to request clarification or add additional context in comments.

12 Comments

Thank you, indeed! Some food for thought in there. One question. That appears to be a generic "promisifier". Should I therefore be sending the function rather than the result, i.e. return await promisifyIDBRequest(() => store.add(JSON.parse(object)));?
Or that first code block should be: idbr.onsuccess = () => resolve(idbr.result)? Then that promisifyIDBRequest function is a generic handler for all IDB requests that have "onsuccess" and "onerror" properties?
@NeilW No, promisifyIDBRequest() expects as input what store.add() returns, and yes, it could be a generic promisifier for anything that has the onsuccess/onerror interface, but in this particular case, the onsuccess handler expects irbr.result for promise resolution, so it's tied into that as well.
I have. And many thanks again. Apart from tidying up my IndexedDb interactions, the reason I was trying to do what I was digging here was because I was not getting JS error info back to .NET via JSInterop. Another key learning is that when rejecting a promise from .onerror, I am better off rejecting (e.target.error) and not just (e). Otherwise the .NET Exception.Message is just [object Event]! Thanks again.
@NeilW Yes, it's redundant. return await somePromise(); and return somePromise() are essentially the same operation, but return await adds one extra step to the overall operation. There even is an ESLint rule for that: no-return-await. It can become useful as soon as you add that try/catch block in addAsync, nicely outlined in jakearchibald.com/2017/await-vs-return-vs-return-await.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.