zid
zid8mo ago

Clerk + Convex: Can't use fetch() in queries and mutations. Please consider using an action

Inside my convex folder, i have a mutation that is creating a user, and if this fails, I want to delete the user from Clerk. I cant use useAction inside this context, so I tried importing it directly which is seemingly okay. I'm using an action as logs suggested, but obviously i'm misunderstanding something. // somewhere inside convex, inside a mutation
import { deleteClerkUser } from "./clerk";

try {
const userId = await createUser({
db,
identity,
});
return userId;
} catch (error) {
// if theres an error, delete clerk user
console.log("clerk user id of deleting user", identity.subject);
try {
await deleteClerkUser({
clerkUserId: identity.subject,
});
} catch (err) {
console.log("deleteClerkUser", err);
throw err;
}
console.log("createUser", error);
throw error;
}
import { deleteClerkUser } from "./clerk";

try {
const userId = await createUser({
db,
identity,
});
return userId;
} catch (error) {
// if theres an error, delete clerk user
console.log("clerk user id of deleting user", identity.subject);
try {
await deleteClerkUser({
clerkUserId: identity.subject,
});
} catch (err) {
console.log("deleteClerkUser", err);
throw err;
}
console.log("createUser", error);
throw error;
}
// convex/clerk.js
"use node";
import { clerkClient } from "@clerk/nextjs/server";

export const deleteClerkUser = action({
args: { clerkUserId: v.string() },
handler: async ({ clerkUserId }) => {
try {
await clerkClient.users.deleteUser(clerkUserId);
} catch (err) {
console.error(err);
return { success: false, error: err.message };
}
},
get handler() {
return this._handler;
},
set handler(value) {
this._handler = value;
},
});
"use node";
import { clerkClient } from "@clerk/nextjs/server";

export const deleteClerkUser = action({
args: { clerkUserId: v.string() },
handler: async ({ clerkUserId }) => {
try {
await clerkClient.users.deleteUser(clerkUserId);
} catch (err) {
console.error(err);
return { success: false, error: err.message };
}
},
get handler() {
return this._handler;
},
set handler(value) {
this._handler = value;
},
});
Error message:
[
{
code: 'unexpected_error',
message: 'Can\'t use fetch() in queries and mutations. Please consider using an action. See https://docs.convex.dev/functions/actions for more details.'
}
]
[
{
code: 'unexpected_error',
message: 'Can\'t use fetch() in queries and mutations. Please consider using an action. See https://docs.convex.dev/functions/actions for more details.'
}
]
32 Replies
lee
lee8mo ago
mutations cannot call actions directly, because they are deterministic and cannot have side effects. You can schedule the action to be run later https://docs.convex.dev/scheduling/scheduled-functions
Scheduled Functions | Convex Developer Hub
Convex allows you to schedule functions to run in the future. This allows you to
zid
zidOP8mo ago
is this the only way? I'm concerned that this approach is more of a hack rather than best practice. does my use-case make sense? I may have made it confusing. i suppose i could call fetch api directly but would love to hear your thoughts on the matter
lee
lee8mo ago
You can choose how to separate the code that is transactional and writes to the database, from the code that could have transient errors and has side-effects. You can schedule the action to run as a scheduled function, but then it might not succeed. You could wrap it in the actions-retrier to improve reliability. Or you could have a cron job that periodically cleans up user state. Oh actually there is a bigger problem. If you end up throwing an error, the entire mutation will rollback. Therefore you can't schedule anything Can you describe more about what you're trying to accomplish with the clerk cleanup?
zid
zidOP8mo ago
My auth flow is: 1. create user with clerk 2. once clerk user is confirmed, create user in db 3. If db transaction fails, rely on convex transaction guarantee to rollback. Additionally, at this point, we delete the user from clerk To delete the clerk user, i need to invoke (at least im assuming from clerk docs) await clerkClient.users.deleteUser(clerkUserId); This method comes from import { clerkClient } from "@clerk/nextjs/server";
lee
lee8mo ago
This seems like a difficult problem. What if instead of deleting the clerk user, you write the convex function so that it won't fail?
zid
zidOP8mo ago
you mean like, re-invoking the db operations until success?
lee
lee8mo ago
Mutations don't fail due to transient errors. The only kinds of failures are deterministic, like if the document doesn't match the schema, and you can prevent this in code Alternatively, from the client you could do
try {
await initUserMutation();
} except (e) {
await deleteClerkUser();
}
try {
await initUserMutation();
} except (e) {
await deleteClerkUser();
}
Which is also slightly weird because if the mutation fails it's likely the action will also fail
zid
zidOP8mo ago
ah i see what you mean. in that sense, my mutations should be safe. i just need to handle the deletion of the clerk user, but leaning into the fact that my mutations should be safe at a fundamental level, it should be safe to not implement a fail-safe measure like deleting the clerk user directly. hmm
lee
lee8mo ago
Sg. For handling deletion of clerk user, i would do if with clerk webhooks
zid
zidOP8mo ago
im using nextjs. what about calling an api route, and handling the deletion from there? also, will convex be roling out their own auth service
lee
lee8mo ago
Sorry i'm not super familiar with auth flows. What do you mean by "handle the deletion of a clerk user"? probably. we're discussing internally
ballingt
ballingt8mo ago
@zid in a sense doing scheduling a Convex action to do the deletion (scheduling can run in less than 1s after it is scheduled, think milliseconds) is safer than a Next API endpoint because you can retry it if it fails
zid
zidOP8mo ago
oh, i just mean (and my apologies if im making this unnecessarily confusing), i need to delete the user that gets created in clerks database because a user's account has been created with an assocated email for example. I need to clean that up hmm i see, let me look at docs
ballingt
ballingt8mo ago
The Next.js API-style way to do this with Convex is - invoke a Convex action from the client, which 1. runs a mutation which deletes the user from Convex 2. does a fetch call to delete it from Clerk and that works totally fine! We suggest clients running mutations and scheduling the action because the mutation will not fail like an action can, if e.g. Clerk's service happens to be down when the user clicks the button. But to get "normal" levels of reliability, you can just do both things in one Convex action. To get really "Convexy" levels of reliability, you run a mutation does the Convex user deletion which schedules an action to take care of the Clerk side. If that action does fail, you have Convex retry logic or you can record that it failed and deal with it once an hour with a cron job or you can just see that failure in the logs and do it manually
lee
lee8mo ago
Automatically Retry Actions
Learn how to automatically retry actions in Convex while also learning a little about scheduling, system tables, and function references.
zid
zidOP8mo ago
okay so it seems like calling scheduled action inside mutation is supported natively and the api looks something like await ctx.scheduler.runAfter touching on the conversation with Lee, it seems like there shouldnt be a concern regarding a mutation failing (assuming the code is solid and tested, of course). in my setup, if db opreations do fail, ill simply be relying on the transactional guarantee where no operations will succeed, hence all i need to take care of is deleting the clerk user in a catch block. From here, it seems like scheduled action could be the best route forward
ballingt
ballingt8mo ago
yep the scheduling of the function is part of this transaction, it will also be "rolled back" (not scheduled) if something fails "scheduled" might be a confusing term here, since you frequently "schedule" something to happen immediately — here scheduled might just means the transaction will complete before this runs. So if the fetch() call does fail, that type of failure has to be dealt with differently than tha automatic Convex mutation transaction stuff.
zid
zidOP8mo ago
try {
await createUserInConvexDB()
} catch(err) {
await deleteUserFromClerkWithAScheduledFn()
}
try {
await createUserInConvexDB()
} catch(err) {
await deleteUserFromClerkWithAScheduledFn()
}
Im getting a little confused (sorry lol) I dont get how deleteUserFromClerkWithAScheduledFn would roll back as its outside the scope of any db operations during the creation of the user in other words, separate transactions, separate queues?
ballingt
ballingt8mo ago
Where is this code? I think you're understanding everything right, you have two options - Run everything from a Convex Action. Do these in either order. I'd write this as
// this is in an action
try {
await deleteUserFromClerk()
} catch (err) {
return; // Deletion wasn't successful, don't delete the user from Convex.
}
await runMutation(api.users.deleteUser)
// this is in an action
try {
await deleteUserFromClerk()
} catch (err) {
return; // Deletion wasn't successful, don't delete the user from Convex.
}
await runMutation(api.users.deleteUser)
* Run things from a mutation
// this is in a mutation
await ctx.db.delete(userId);
await ctx.scheduler.runAfter(
0,
internal.users.deleteUser,
{ userId }
);
// this is in a mutation
await ctx.db.delete(userId);
await ctx.scheduler.runAfter(
0,
internal.users.deleteUser,
{ userId }
);
If you do the second and you're worried the Clerk delete might fail, then you should retry that action, like the https://stack.convex.dev/retry-actions link Lee posted. Or you could put the retry in the action like
for (let i = 0; i<10; i++) {
try {
await fetch('http://clerk.com/delete-my-user');
} catch (err) {
continue;
}
break;
}
for (let i = 0; i<10; i++) {
try {
await fetch('http://clerk.com/delete-my-user');
} catch (err) {
continue;
}
break;
}
If you don't want to delete the user in Convex until after the Clerk thing is successful, then do them in that order
zid
zidOP8mo ago
I think our starting points are a bit different. The starting point for me is that the transaction (batch of writes) when attempting to create the user in convex has failed, and thus nothing has been created in convex, so nothing to delete. The only thing that happened was an error. It's this error, and its respective catch block, where I attempt to delete the user in Clerk's database, as this was created first.
ballingt
ballingt8mo ago
Ah so this isn't the user clicking the "delete me" button, it's the user clicking a "sign up" button but something about the Clerk claims makes the Convex user creation flow fail? sorry I should have read more of this thread
zid
zidOP8mo ago
nono, no worries at all, much appreciated so 1. user gets created successfully in Clerk 2. my app takes the newly created clerk's userId and attempts to create a user in convex 3. Its step 2 that i want to create a fail-safe measure
ballingt
ballingt8mo ago
Similarly you can run this as an action that calls a mutation that creates the user (if this fails, go delete the user in clerk) or a mutation that schedules an action if some condition is true
zid
zidOP8mo ago
from lees conversation, one of the main conclusions was that a mutation failing is virtually impossible, provided its not a deterministic error so this might be a bit overkill, but yea
ballingt
ballingt8mo ago
Are there many ways you're worried about the user creation failing that you can't anticipate? I'd use if statements to decide if the user creation ought to succeed or not, and schedule the function to delete the user if not. But I think you've correctly identified that you can't both schedule a function AND use the automatic Convex "rollback" (not applying the transaction) when an error is thrown — this could be real shortcoming in some cases where you'd need to use an Action to catch the error and decide what to do
from lees conversation, one of the main conclusions was that a mutation failing is virtually impossible, provided its not a deterministic error
Yes So I agree you don't need this, just use if statements to decide whether you want to create this user or not instead of saying "any error that happens, programmer error or validation error or the clerk users belongs to the wrong team, throw an error" But if you do want to delete the clerk user no matter what in any error case, they I'd use the action approach. Currently throwing errors as control flow in Convex is limited in this way, there's only one "level" of this: everything will be rolled back in a mutation (including the scheduling of your clerk deletion fetch action), not just a specific section of code.
zid
zidOP8mo ago
i think we're on the same page, lol i guess to really understand at the level i want to, i would need to understand everything will be rolled back in a mutation, not a specific section of code. more deeply. Just more into the mechanics of how that works at a lower level. To be clear, im not asking this of you haha. i understand enough to move forward. Thanks a bunch.
ballingt
ballingt8mo ago
it's not too bad, just every time you run a mutation in convex we don't actually apply persist the changes to the database until the end (but it looks to the function running like they're applied). If an error is thrown (and not caught, I mean if an error propagates all the way up), we don't apply these changes to the database.
zid
zidOP8mo ago
oh okay, right i think i forgot that these operations are happening all underneath a single mutation/fn
ballingt
ballingt8mo ago
I'd say that what you want is an explicit transaction boundary, or a subtransaction. Today we do these by running an action: an action can call as many mutations as it wants, but each one is its own transaction. Once one of these mutations finishes you can't roll that one back, so you lose some of the magic of putting related code all together.
zid
zidOP8mo ago
that makes sense. although, earlier i tried to create an action for the fail-safe delete clerk fn but ran into some issue regarding where and how actions can be called. I'll have to read the docs.
ballingt
ballingt8mo ago
perhaps you tried to call it directly, you can only schedule actions from mutations in Convex
zid
zidOP8mo ago
yea,that was it lol ah, alrighty, thanks again @ballingt @lee . Super helpful.

Did you find this page helpful?