Handling race conditions when "deleting" or modifying documents
We're running into a problem where we have a delete mutation that is fired twice if a user rapidly hits the button causing convex to throw an error.
Is there a recommended approach for handling situations like this?
4 Replies
A few things, first is UI: you could disable the button on click either via some local state or a Convex optimistic update — you're probably already thinking of this, and it's not always possible (what happens if you click the button in two different windows?) but it's a good start.
But it sounds like in the mutation you should succeed if the item is already deleted. That would mean checking for the item before deleting it, only deleting if it exists.
If you're not doing the UI solution yet, I'd start with that — making the Convex mutation idempotent has a slight debugging cost because you'd no longer get an exception when there should be one, e.g. a programmer error where the wrong (nonexistent) item is being deleted.
Makes sense. On this note related to error handling do we think we'll see something similar to an onError callback eventually land in convex?
Worth mentioning: If you have two mutations running at roughly the same time, they won't both be able to see the document and delete it. Our serializable isolation guarantee makes it such that each mutation will either succeed in deleting it or will not see it in the DB.
If you want to handle idempotency robustly, you can make a client-side key, possibly when the form is rendered, and pass it along in the mutation, so that every click of the button passes up the same key. Financial APIs like Square have affordances like this so you don't double-bill, for instance.
I'm also interested in an onError callback - but before I prime you with my thoughts, do you want this to execute on the client or in a mutation / action / something else?
On the client you can toss a try/catch around an awaited mutation, and server-side you could try/catch and schedule something, but it'll only get scheduled if the mutation returns (and doesn't re-throw), so it wouldn't roll back all the other things you may have done to the DB
So a try/catch around the mutation on the client sounds like what I'm looking for but this might require me adding some additional boilerplate everywhere especially if I have multiple spots where the mutation is called. I could probably create a custom hook but it would be nice if I can avoid this for every mutation