David Alonso
David Alonsoβ€’2mo ago

Mutation error: `Returned promise will never resolve` caused by triggers

I've never seen this error before, and when i get rid of this trigger in my blocks mutation it goes away... The original mutation contains some promise all statements that insert docs into the blocks table Any ideas?
triggers.register("blocks", async (ctx, change) => {
if (change.operation === "insert") {
const newBlockFid = parseFid(change.newDoc.fid, "blocks");
// Try to fetch block with same fid - which will throw if there's multiple. Note that at the time of running this query the new doc is returned by the query
await _getBlockByFid(ctx, {
blockFid: newBlockFid,
});
} else if (change.operation === "update") {
// Throw an error if the fid is being updated
if (change.oldDoc.fid !== change.newDoc.fid) {
throw new ConvexError("Block FID cannot be updated");
}
}
});
triggers.register("blocks", async (ctx, change) => {
if (change.operation === "insert") {
const newBlockFid = parseFid(change.newDoc.fid, "blocks");
// Try to fetch block with same fid - which will throw if there's multiple. Note that at the time of running this query the new doc is returned by the query
await _getBlockByFid(ctx, {
blockFid: newBlockFid,
});
} else if (change.operation === "update") {
// Throw an error if the fid is being updated
if (change.oldDoc.fid !== change.newDoc.fid) {
throw new ConvexError("Block FID cannot be updated");
}
}
});
38 Replies
Convex Bot
Convex Botβ€’2mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
David Alonso
David AlonsoOPβ€’2mo ago
also it took me a very long time to find that the issue was related to the triggers, so a better error log would be very helpful in the future
lee
leeβ€’2mo ago
Hmm. This error indicates that there's a deadlock (which is why it's so opaque; as far as Convex knows, the function just never returned). It's probably caused by the lock Triggers uses to guard against parallel updates, but I don't see how it could happen. Can you share more of the code so I can try to repro?
David Alonso
David AlonsoOPβ€’2mo ago
sent you some more details through a dm! btw is there a way to see the sync status of triggers in the dashboard? similar to regular functions
lee
leeβ€’2mo ago
What do you mean sync status? Triggers run inline within the mutation. You can do a console.log if you want to know that it's running
David Alonso
David AlonsoOPβ€’2mo ago
Ah sorry i mean seeing the source code that has been deployed for triggers Getting the same error here:
export const queueEdits = authenticatedMutation({
args: {
edits: v.array(vFid("firestoreEdits")),
},
handler: async (ctx, args) => {
const uniqueEdits = [...new Set(args.edits)];

// Filter so that we only queue edits that are in "uncommitted" or "error" status
const editsToQueue = await Promise.all(
uniqueEdits.map((editId) =>
_getFirestoreEditByFid(ctx, { editFid: editId })
)
).then((edits) =>
edits.filter(
(edit) => edit.status === "uncommitted" || edit.status === "error"
)
);

return await _updateEditsStatus(ctx, {
edits: editsToQueue,
status: "queued",
});
},
});
export const _updateEditsStatus = internalMutation({
args: {
edits: v.array(doc(fireviewSchema, "firestoreEdits")),
status: zodToConvex(zFirestoreDataChangeStatus),
},
handler: async (ctx, args) => {
// Chunk the edits to avoid overwhelming the system
const chunks = chunk(args.edits, 50);

for (const editChunk of chunks) {
await Promise.all(
editChunk.map(async (edit) => {
await ctx.db.patch(edit._id, {
status: args.status,
});
})
);
}
},
});
export const queueEdits = authenticatedMutation({
args: {
edits: v.array(vFid("firestoreEdits")),
},
handler: async (ctx, args) => {
const uniqueEdits = [...new Set(args.edits)];

// Filter so that we only queue edits that are in "uncommitted" or "error" status
const editsToQueue = await Promise.all(
uniqueEdits.map((editId) =>
_getFirestoreEditByFid(ctx, { editFid: editId })
)
).then((edits) =>
edits.filter(
(edit) => edit.status === "uncommitted" || edit.status === "error"
)
);

return await _updateEditsStatus(ctx, {
edits: editsToQueue,
status: "queued",
});
},
});
export const _updateEditsStatus = internalMutation({
args: {
edits: v.array(doc(fireviewSchema, "firestoreEdits")),
status: zodToConvex(zFirestoreDataChangeStatus),
},
handler: async (ctx, args) => {
// Chunk the edits to avoid overwhelming the system
const chunks = chunk(args.edits, 50);

for (const editChunk of chunks) {
await Promise.all(
editChunk.map(async (edit) => {
await ctx.db.patch(edit._id, {
status: args.status,
});
})
);
}
},
});
The only trigger for this table is essentially the same as what I DM'd you before for the other tables i should say both authenticatedMutation and internalMutation are wrapped by triggers The only way for us to use aggregates robustly is in conjunction with triggers but this is blocking that so it would be great to understand how to fix this
Riki
Rikiβ€’2mo ago
I have the same error. The "funny" part is that even if I edit the code of the trigger to do nothing (no query nor mutations), I get this error:
// testTrigger.ts
import {triggers} from './shared/triggers';

triggers.register('users', async (ctx, change) => {
// Do nothing
});
// testTrigger.ts
import {triggers} from './shared/triggers';

triggers.register('users', async (ctx, change) => {
// Do nothing
});
// function.ts
import {
mutation as rawMutation,
internalMutation as rawInternalMutation,
} from '../_generated/server';
import {customCtx, customMutation} from 'convex-helpers/server/customFunctions';
import {triggers} from './triggers';
import '../derivedUserFriends';
import '../testTrigger'; // THIS IMPORT CAUSES THE AFOREMENTIONED ERROR

export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(
rawInternalMutation,
customCtx(triggers.wrapDB),
);
// function.ts
import {
mutation as rawMutation,
internalMutation as rawInternalMutation,
} from '../_generated/server';
import {customCtx, customMutation} from 'convex-helpers/server/customFunctions';
import {triggers} from './triggers';
import '../derivedUserFriends';
import '../testTrigger'; // THIS IMPORT CAUSES THE AFOREMENTIONED ERROR

export const mutation = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(
rawInternalMutation,
customCtx(triggers.wrapDB),
);
Given I have another trigger that works and this trigger does nothing, I suspect this is something related to the mutation that happens within the 'users' table that is the issue (but it works well without the trigger)
lee
leeβ€’2mo ago
Is there a chance one of you could publish a small repro to github / pastebin? This definitely sounds like a triggers bug, but I'm kind of stumped unless I can reproduce it myself so far the only way i've found to repro the error is something like this (wrapping it twice)
export const mutation1 = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation1 = customMutation(
rawInternalMutation,
customCtx(triggers.wrapDB),
);

export const mutation = customMutation(mutation1, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(
internalMutation1,
customCtx(triggers.wrapDB),
);
export const mutation1 = customMutation(rawMutation, customCtx(triggers.wrapDB));
export const internalMutation1 = customMutation(
rawInternalMutation,
customCtx(triggers.wrapDB),
);

export const mutation = customMutation(mutation1, customCtx(triggers.wrapDB));
export const internalMutation = customMutation(
internalMutation1,
customCtx(triggers.wrapDB),
);
David Alonso
David AlonsoOPβ€’2mo ago
ah right, i have wrapped both authenticatedMutation and internalMutation with triggers, and i think the issue occurs when i call an internal mutation from an auth one how do i fix this best?
lee
leeβ€’2mo ago
how are you calling the internal mutation from the auth one? with ctx.runMutation?
lee
leeβ€’2mo ago
David Alonso
David AlonsoOPβ€’2mo ago
no just await _functionname
lee
leeβ€’2mo ago
Calling mutations directly as functions is not supported, and custom functions is one of the reasons why The two options are to refactor out the handler (as described in best practices) or to call ctx.runMutation, which has extra overhead
David Alonso
David AlonsoOPβ€’2mo ago
this is not the case with calling internal queries directly right?
Calling mutations directly as functions is not supported
is there a good way to catch this during development? idk if this is possible with eslint rules...
lee
leeβ€’2mo ago
Calling internal queries directly is also not supported. doing so will not cause problems with Triggers, but there may be other problems
lee
leeβ€’2mo ago
Eslint would be great, but I'm not sure how such a rule would work. We're looking into it. For triggers we can throw a better error https://github.com/get-convex/convex-helpers/pull/348 which would have caught it in your case.
GitHub
nice error on nested triggers by ldanilek Β· Pull Request #348 Β· get...
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
David Alonso
David AlonsoOPβ€’2mo ago
nice that's already very helpful, but for queries i've never run into any issues so it's hard to detect i just want to make sure i understand correctly where the issue is: calling custom functions from custom functions in general? or only certain cases like mutations from mutations or queries from mutations, etc and how can i quantify the overhead of ctx.runMutation / ctx.runQuery? is it mostly due to arg validation?
lee
leeβ€’2mo ago
the issue is calling any convex function directly from another convex function. ctx.runMutation / ctx.runQuery start up a new javascript environment, serde the args and return values, and do arg validation. We don't recommend using them unless you're writing a component. The real recommendation is https://docs.convex.dev/production/best-practices/#use-helper-functions-to-write-shared-code
Best Practices | Convex Developer Hub
Here's a collection of our recommendations on how best to use Convex to build
lee
leeβ€’2mo ago
are there any docs that indicated calling a convex function directly as if it were a function would work? We're trying to track down such docs.
David Alonso
David AlonsoOPβ€’2mo ago
Don’t think so, it just felt like the most concise way to reuse code TS errors would definitely help instantly break the habit
lee
leeβ€’2mo ago
absolutely, i'm trying those out now πŸ‘
David Alonso
David AlonsoOPβ€’2mo ago
For actions, sharing code via helper functions instead of using ctx.runAction reduces function calls and resource usage.
I guess this shouldn't just be actions then in the docs
lee
leeβ€’2mo ago
true, good call out
David Alonso
David AlonsoOPβ€’2mo ago
what about calling convex functions from triggers? I assume that's not any different
Clever Tagline
Clever Taglineβ€’2mo ago
For a while I've been a little foggy on what such "helper functions" might look like, but I think I saw an example in a video posted to the Convex YT channel last night. Jamie walks through the recreation of a Pokemon-related app that was demonstrated on Theo's channel. Here's a link to the relevant part of the video where he mentions this helper (updateTally). Having an example like this in the docs would be really helpful IMO.
Convex
YouTube
Porting Theo's T3 Stack Roundest to Convex
In a recent video (https://www.youtube.com/watch?v=O-EWIlZW0mM), Theo Browne built a "rate the roundest Pokemon" app using five different stacks. In a shocking turn of events, Convex was not one of the five. In this video, Convex co-founder Jamie Turner walks through what it took to port the tRPC version to Convex. Spoiler: not much. And we end...
David Alonso
David AlonsoOPβ€’2mo ago
thanks for sharing!
lee
leeβ€’2mo ago
The docs have example helper functions ensureTeamAdmin and getCurrentUser. How can we improve the example?
Clever Tagline
Clever Taglineβ€’5w ago
I had to go digging to find where those examples were shown. Ironically they're right under the text that @David Alonso quoted, which comes from the "Best Practices" page under "Production". 🫒 Admittedly I don't recall reading that page yet (I thought I had, but those functions don't look familiar), and I'm trying to figure out why. First off, I guess I've seen the phrase "helper functions" enough that I thought that the text David quoted was from some other part of the docs that I had read. That aside, I'm guessing that I missed it because my approach to docs is to read the first few pages/sections to get the core info that I need, then to refer to the rest on an as-needed basis; e.g. to brush up on specific features. For me, I'd love to see the whole "Best Practices" page surfaced higher in the docs hierarchy. It feels buried in its current location, plus I feel that a list of best practices is useful for all phases of development, not just production. If I'd seen those example functions earlier in my introduction to Convex, it would have been really helpful as I was mulling over how to solve certain problems.
lee
leeβ€’5w ago
makes sense. i think we're planning on reorganizing docs like this. (i also linked that section twice in this thread πŸ˜› ) Jamie's video is also a good example though. i watched it last night πŸ˜„
jamwt
jamwtβ€’5w ago
I also had a postmortem session with @Tom Redman where I learned a lot of tricks about presenting code more clearly. I'm pretty excited for the next video, which I think is going to be excellent slowly figuring things out!
jamwt
jamwtβ€’5w ago
GitHub
1app5stacks-convex/convex-version/convex/pokemon.ts at main Β· jamwt...
Theo built the same app 5 times because he's dumb (Jamie built it a 6th time, also dumb) - jamwt/1app5stacks-convex
jamwt
jamwtβ€’5w ago
with updateTally being said helper function
Clever Tagline
Clever Taglineβ€’5w ago
My bad. I've been mostly following the conversation in this thread, not necessarily following all links.
David Alonso
David AlonsoOPβ€’4w ago
does this seem like a promising path? trying to decide how much time to invest on our side on eslint rules and other guards
lee
leeβ€’4w ago
Working on it, but it may take a while. The change is easy, but we want to see how many projects will be affected
David Alonso
David AlonsoOPβ€’4w ago
Is there a way we could somehow opt into it then?
lee
leeβ€’4w ago
Not that i can think of -- unless you want a custom build of the convex package
djbalin
djbalinβ€’2w ago
Could calculating/populating your aggregates e.g. once every 24 hours work in your situation? That's what we are doing in our project. We have some aggregates on a table that runs very hot write-wise, so we decided to not attach an aggregate-updating trigger to that table due to the high write volume. Instead we just update our aggregates with new documents from the last 24 hours once nightly. Of course this only works if you don't need very real-time aggregates.