dannyelo
dannyeloβ€’3mo ago

runAfter is not throwing an error on the client

Hello, can't understand why error is not throwing to the client. This is the pattern I'm trying to implement. I can see the Error 'Called getCurrentUser without authentication present' logged in convex, but I can't get it in the front end. I'm using Next.js. convex/customers.ts
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
const organization = await getCurrentOrganization(ctx)
return await upsertCustomerWithFacturapi(ctx, {
...args,
organizationId: organization._id,
})
},
})

async function upsertCustomerWithFacturapi(
ctx: MutationCtx,
args: CustomerArgs,
): Promise<any> {
try {
return await ctx.scheduler.runAfter(
0,
api.customers_usenode.upsertCustomerWithFacturapi,
{
...args,
},
)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
}
}
}
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
const organization = await getCurrentOrganization(ctx)
return await upsertCustomerWithFacturapi(ctx, {
...args,
organizationId: organization._id,
})
},
})

async function upsertCustomerWithFacturapi(
ctx: MutationCtx,
args: CustomerArgs,
): Promise<any> {
try {
return await ctx.scheduler.runAfter(
0,
api.customers_usenode.upsertCustomerWithFacturapi,
{
...args,
},
)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
}
}
}
convex/customers_usenode.ts
'use node'
export const upsertCustomerWithFacturapi = action({
args: customerArgs,
handler: async (ctx, args) => {
const { customerData, organizationId, customerId } = args

const facturapiCustomerData = {
// object...
}

try {
const facturapi = await getFacturapiLiveInstance({ ctx })
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
}
}
},
})
'use node'
export const upsertCustomerWithFacturapi = action({
args: customerArgs,
handler: async (ctx, args) => {
const { customerData, organizationId, customerId } = args

const facturapiCustomerData = {
// object...
}

try {
const facturapi = await getFacturapiLiveInstance({ ctx })
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
}
}
},
})
convex/facturapi_usenode.ts
'use node'
export const getFacturapiLiveInstance = async ({
ctx,
}: {
ctx: ActionCtx
}): Promise<Facturapi> => {
try {
const currentUserOrganization = await ctx.runQuery(
internal.organizations.getOrganizationFacturapiKeys,
)

const facturapi = new Facturapi()
return facturapi
} catch (error) {
throw new Error('Error getting Facturapi instance')
}
}
'use node'
export const getFacturapiLiveInstance = async ({
ctx,
}: {
ctx: ActionCtx
}): Promise<Facturapi> => {
try {
const currentUserOrganization = await ctx.runQuery(
internal.organizations.getOrganizationFacturapiKeys,
)

const facturapi = new Facturapi()
return facturapi
} catch (error) {
throw new Error('Error getting Facturapi instance')
}
}
76 Replies
Convex Bot
Convex Botβ€’3mo 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!
dannyelo
dannyeloOPβ€’3mo ago
convex/organizations.ts
export const getOrganizationFacturapiKeys = internalQuery({
args: {},
handler: async (ctx, args) => {
const currentOrganization = await getCurrentOrganization(ctx)

if (!currentOrganization) {
throw new Error('Current organization not found')
}

return {
facturapiTestApiKey: currentOrganization.facturapiTestApiKey,
facturapiLiveApiKey: currentOrganization.facturapiLiveApiKey,
facturapiOrganizationId: currentOrganization.facturapiOrganizationId,
}
},
})

export const getCurrentOrganization = async (ctx: QueryCtx | MutationCtx) => {
const user = await getCurrentUser(ctx)
const currentUserOrganization = await ctx.db
.query('user_organizations')
.filter((q) => q.eq(q.field('userId'), user._id))
.filter((q) => q.eq(q.field('isCurrent'), true))
.unique()

if (!currentUserOrganization) {
throw new Error('Current user organization not found')
}

const organization = await ctx.db.get(currentUserOrganization.organizationId)

if (!organization) {
throw new Error('Organization not found')
}

return { ...organization, currentUser: { ...currentUserOrganization } }
}
export const getOrganizationFacturapiKeys = internalQuery({
args: {},
handler: async (ctx, args) => {
const currentOrganization = await getCurrentOrganization(ctx)

if (!currentOrganization) {
throw new Error('Current organization not found')
}

return {
facturapiTestApiKey: currentOrganization.facturapiTestApiKey,
facturapiLiveApiKey: currentOrganization.facturapiLiveApiKey,
facturapiOrganizationId: currentOrganization.facturapiOrganizationId,
}
},
})

export const getCurrentOrganization = async (ctx: QueryCtx | MutationCtx) => {
const user = await getCurrentUser(ctx)
const currentUserOrganization = await ctx.db
.query('user_organizations')
.filter((q) => q.eq(q.field('userId'), user._id))
.filter((q) => q.eq(q.field('isCurrent'), true))
.unique()

if (!currentUserOrganization) {
throw new Error('Current user organization not found')
}

const organization = await ctx.db.get(currentUserOrganization.organizationId)

if (!organization) {
throw new Error('Organization not found')
}

return { ...organization, currentUser: { ...currentUserOrganization } }
}
convex/users.ts
export const getCurrentUser = async (ctx: QueryCtx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Called getCurrentUser without authentication present')
// this error I see in convex logs
}

const user = await ctx.db
.query('users')
.withIndex('by_token', (q) =>
q.eq('tokenIdentifier', identity.tokenIdentifier),
)
.unique()

if (!user) {
throw new Error('User not found')
}

return user
}
export const getCurrentUser = async (ctx: QueryCtx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Called getCurrentUser without authentication present')
// this error I see in convex logs
}

const user = await ctx.db
.query('users')
.withIndex('by_token', (q) =>
q.eq('tokenIdentifier', identity.tokenIdentifier),
)
.unique()

if (!user) {
throw new Error('User not found')
}

return user
}
jamalsoueidan
jamalsoueidanβ€’3mo ago
You just throw error in your convex functions and catch them in your frontend.
dannyelo
dannyeloOPβ€’3mo ago
I think I'm throwing them, aren't I?
jamalsoueidan
jamalsoueidanβ€’3mo ago
where do you call those convex function in the frontend
dannyelo
dannyeloOPβ€’3mo ago
const onSubmit = async (data: z.infer<typeof CustomerFormSchema>) => {
try {
const result = await upsertCustomer({
// ... object sended
})

console.log('upsertCustomer result:', result)

// If result is falsy or an error object, throw an error
if (!result || (typeof result === 'object' && 'error' in result)) {
throw new Error(result?.error || 'An unknown error occurred')
}

customerForm.reset()
toast({
title: selectedCustomer ? 'Cliente actualizado' : 'Cliente creado',
description: selectedCustomer
? 'Cliente actualizado exitosamente'
: 'Cliente creado exitosamente',
duration: 5000,
variant: 'default',
})
onClose()
} catch (error) {
console.error('Error in onSubmit:', error)
toast({
title: 'Error',
description: error instanceof Error ? error.message : String(error),
duration: 5000,
variant: 'destructive',
})
}
}
const onSubmit = async (data: z.infer<typeof CustomerFormSchema>) => {
try {
const result = await upsertCustomer({
// ... object sended
})

console.log('upsertCustomer result:', result)

// If result is falsy or an error object, throw an error
if (!result || (typeof result === 'object' && 'error' in result)) {
throw new Error(result?.error || 'An unknown error occurred')
}

customerForm.reset()
toast({
title: selectedCustomer ? 'Cliente actualizado' : 'Cliente creado',
description: selectedCustomer
? 'Cliente actualizado exitosamente'
: 'Cliente creado exitosamente',
duration: 5000,
variant: 'default',
})
onClose()
} catch (error) {
console.error('Error in onSubmit:', error)
toast({
title: 'Error',
description: error instanceof Error ? error.message : String(error),
duration: 5000,
variant: 'destructive',
})
}
}
I have a form, this is the onSubmit function
jamalsoueidan
jamalsoueidanβ€’3mo ago
What is upsertCustomer
dannyelo
dannyeloOPβ€’3mo ago
No matter the backend is throwing an error, I always get the toast with 'Cliente creado' (customer created).
jamalsoueidan
jamalsoueidanβ€’3mo ago
What is upsertCustomer ?
dannyelo
dannyeloOPβ€’3mo ago
upsertCustomer is create a customer if it doesnt exist or update the customer if exist
jamalsoueidan
jamalsoueidanβ€’3mo ago
i know, where do you define it??? const upsertCustomer = useMutation(....????
dannyelo
dannyeloOPβ€’3mo ago
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
const organization = await getCurrentOrganization(ctx)
return await upsertCustomerWithFacturapi(ctx, {
...args,
organizationId: organization._id,
})
},
})

async function upsertCustomerWithFacturapi(
ctx: MutationCtx,
args: CustomerArgs,
): Promise<any> {
try {
return await ctx.scheduler.runAfter(
0,
api.customers_usenode.upsertCustomerWithFacturapi,
{
...args,
},
)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
}
}
}
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
const organization = await getCurrentOrganization(ctx)
return await upsertCustomerWithFacturapi(ctx, {
...args,
organizationId: organization._id,
})
},
})

async function upsertCustomerWithFacturapi(
ctx: MutationCtx,
args: CustomerArgs,
): Promise<any> {
try {
return await ctx.scheduler.runAfter(
0,
api.customers_usenode.upsertCustomerWithFacturapi,
{
...args,
},
)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error),
}
}
}
const upsertCustomer = useMutation(api.customers.upsertCustomer) I define it at the begining of the component
jamalsoueidan
jamalsoueidanβ€’3mo ago
Thank you Change this:
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
throw new Error("not working");
},
})
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
throw new Error("not working");
},
})
and test your frontend now, and see if you get an error πŸ™‚ This way you validate that the backend can indeed throw error.
dannyelo
dannyeloOPβ€’3mo ago
Ok I will try that noe now
jamalsoueidan
jamalsoueidanβ€’3mo ago
This code is wrong:
async function upsertCustomerWithFacturapi(
ctx: MutationCtx,
args: CustomerArgs,
): Promise<any> {
try {
return await ctx.scheduler.runAfter( //ONLY RETURN ID of the scheduler, it does NOT return the data and cant throw error
0,
api.customers_usenode.upsertCustomerWithFacturapi,
{
...args,
},
)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error), //this will never happen
}
}
async function upsertCustomerWithFacturapi(
ctx: MutationCtx,
args: CustomerArgs,
): Promise<any> {
try {
return await ctx.scheduler.runAfter( //ONLY RETURN ID of the scheduler, it does NOT return the data and cant throw error
0,
api.customers_usenode.upsertCustomerWithFacturapi,
{
...args,
},
)
} catch (error) {
return {
error: error instanceof Error ? error.message : String(error), //this will never happen
}
}
dannyelo
dannyeloOPβ€’3mo ago
Yes, thats why I saw that all the code in runAfter is not throwing any Error. Do you know how can I solve this?
jamalsoueidan
jamalsoueidanβ€’3mo ago
well if those are indeed schedules, then you need to create additional table where you save those ids, and after that update the frontend with the results...if error you show RED notification, or green depending on when it finishs. im not sure what you trying to accomplish here.
dannyelo
dannyeloOPβ€’3mo ago
I'm still trying to figure it out the convex patterns and architecture. The only solution I found to run an action from a mutation is the runAfter.
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
// throw new Error('not working')
if (!args.customerData.alias) {
throw new Error('Alias is required')
}

const organization = await getCurrentOrganization(ctx)

const hasRequiredFacturapiFields =
args.customerData.legalName &&
args.customerData.taxId &&
args.customerData.taxSystem &&
args.customerData.address?.zip

if (hasRequiredFacturapiFields) {
return await upsertCustomerWithFacturapi(ctx, {
...args,
organizationId: organization._id,
})
}
console.log('upsertCustomerWithoutFacturapi')
await upsertCustomerWithoutFacturapi(ctx, {
...args,
organizationId: organization._id,
})
},
})
export const upsertCustomer = mutation({
args: customerArgs,
handler: async (ctx, args) => {
// throw new Error('not working')
if (!args.customerData.alias) {
throw new Error('Alias is required')
}

const organization = await getCurrentOrganization(ctx)

const hasRequiredFacturapiFields =
args.customerData.legalName &&
args.customerData.taxId &&
args.customerData.taxSystem &&
args.customerData.address?.zip

if (hasRequiredFacturapiFields) {
return await upsertCustomerWithFacturapi(ctx, {
...args,
organizationId: organization._id,
})
}
console.log('upsertCustomerWithoutFacturapi')
await upsertCustomerWithoutFacturapi(ctx, {
...args,
organizationId: organization._id,
})
},
})
jamalsoueidan
jamalsoueidanβ€’3mo ago
What is upsertCustomerWithFacturapi doing? calling third party api?
dannyelo
dannyeloOPβ€’3mo ago
Im running that action conditionally Yes If (hasRequiredFacturapiFields) --> calls action (external api) if not --> create a mutation in convex
jamalsoueidan
jamalsoueidanβ€’3mo ago
so you want to show failure in case the third party api fails ?
dannyelo
dannyeloOPβ€’3mo ago
Yes!
jamalsoueidan
jamalsoueidanβ€’3mo ago
api.customers_usenode.upsertCustomerWithFacturapi where is this code
dannyelo
dannyeloOPβ€’3mo ago
export const upsertCustomerWithFacturapi = action({
args: customerArgs,
handler: async (ctx, args) => {
const { customerData, organizationId, customerId } = args

const facturapiCustomerData = {
legal_name: customerData.legalName,
tax_id: customerData.taxId,
tax_system: customerData.taxSystem,
email: customerData.email,
phone: customerData.phone,
},
}

try {
const facturapi = await getFacturapiLiveInstance({ ctx })

if (!facturapi) {
throw new Error('Error getting Facturapi instance')
}

if (!customerId) {
// Create new customer in Facturapi and Convex
const facturapiCustomer = await facturapi.customers.create(
facturapiCustomerData,
)
console.log('facturapiCustomer', facturapiCustomer)
await ctx.runMutation(api.customers.createCustomerInConvex, {
customerData,
organizationId,
facturapiCustomerId: facturapiCustomer.id,
})
console.log('mutationRunned', facturapiCustomer)
} else {
// Update customer in Facturapi and Convex
const updatedFacturapiCustomer = await facturapi.customers.update(
customerId,
facturapiCustomerData,
)

if (updatedFacturapiCustomer.id) {
await ctx.runMutation(api.customers.updateCustomerInConvex, {
customerId,
customerData,
organizationId,
})
}
}
} catch (error) {
console.log('Entered the error kkk', error)
return {
error: error instanceof Error ? error.message : String(error),
}
}
},
})
export const upsertCustomerWithFacturapi = action({
args: customerArgs,
handler: async (ctx, args) => {
const { customerData, organizationId, customerId } = args

const facturapiCustomerData = {
legal_name: customerData.legalName,
tax_id: customerData.taxId,
tax_system: customerData.taxSystem,
email: customerData.email,
phone: customerData.phone,
},
}

try {
const facturapi = await getFacturapiLiveInstance({ ctx })

if (!facturapi) {
throw new Error('Error getting Facturapi instance')
}

if (!customerId) {
// Create new customer in Facturapi and Convex
const facturapiCustomer = await facturapi.customers.create(
facturapiCustomerData,
)
console.log('facturapiCustomer', facturapiCustomer)
await ctx.runMutation(api.customers.createCustomerInConvex, {
customerData,
organizationId,
facturapiCustomerId: facturapiCustomer.id,
})
console.log('mutationRunned', facturapiCustomer)
} else {
// Update customer in Facturapi and Convex
const updatedFacturapiCustomer = await facturapi.customers.update(
customerId,
facturapiCustomerData,
)

if (updatedFacturapiCustomer.id) {
await ctx.runMutation(api.customers.updateCustomerInConvex, {
customerId,
customerData,
organizationId,
})
}
}
} catch (error) {
console.log('Entered the error kkk', error)
return {
error: error instanceof Error ? error.message : String(error),
}
}
},
})
The error is inside const facturapi = await getFacturapiLiveInstance({ ctx })
export const getFacturapiLiveInstance = async ({
ctx,
}: {
ctx: ActionCtx
}): Promise<Facturapi> => {
try {
const currentUserOrganization = await ctx.runQuery(
internal.organizations.getOrganizationFacturapiKeys,
)

const facturapi = new Facturapi()
return facturapi
} catch (error) {
throw new Error('Error getting Facturapi instance')
}
}
export const getFacturapiLiveInstance = async ({
ctx,
}: {
ctx: ActionCtx
}): Promise<Facturapi> => {
try {
const currentUserOrganization = await ctx.runQuery(
internal.organizations.getOrganizationFacturapiKeys,
)

const facturapi = new Facturapi()
return facturapi
} catch (error) {
throw new Error('Error getting Facturapi instance')
}
}
Then I'm calling here getOrganizationFacturapiKeys
export const getOrganizationFacturapiKeys = internalQuery({
args: {},
handler: async (ctx, args) => {
const currentOrganization = await getCurrentOrganization(ctx)

if (!currentOrganization) {
throw new Error('Current organization not found')
}

return {
// facturapi keys are returned here
}
},
})
export const getOrganizationFacturapiKeys = internalQuery({
args: {},
handler: async (ctx, args) => {
const currentOrganization = await getCurrentOrganization(ctx)

if (!currentOrganization) {
throw new Error('Current organization not found')
}

return {
// facturapi keys are returned here
}
},
})
Then I'm calling getCurrentOrganization here
export const getCurrentOrganization = async (ctx: QueryCtx | MutationCtx) => {
const user = await getCurrentUser(ctx)
const currentUserOrganization = await ctx.db
.query('user_organizations')
.filter((q) => q.eq(q.field('userId'), user._id))
.filter((q) => q.eq(q.field('isCurrent'), true))
.unique()

if (!currentUserOrganization) {
throw new Error('Current user organization not found')
}

const organization = await ctx.db.get(currentUserOrganization.organizationId)

if (!organization) {
throw new Error('Organization not found')
}

return { ...organization, currentUser: { ...currentUserOrganization } }
}
export const getCurrentOrganization = async (ctx: QueryCtx | MutationCtx) => {
const user = await getCurrentUser(ctx)
const currentUserOrganization = await ctx.db
.query('user_organizations')
.filter((q) => q.eq(q.field('userId'), user._id))
.filter((q) => q.eq(q.field('isCurrent'), true))
.unique()

if (!currentUserOrganization) {
throw new Error('Current user organization not found')
}

const organization = await ctx.db.get(currentUserOrganization.organizationId)

if (!organization) {
throw new Error('Organization not found')
}

return { ...organization, currentUser: { ...currentUserOrganization } }
}
Then I'm calling getCurrentUser and inside here is where I see the error in convex
jamalsoueidan
jamalsoueidanβ€’3mo ago
you define upsertCustomerWithFacturapi as action, but calling it directly from mutation function, which you should not do. the way the functions should call each other is through ctx.runAction, ctx.runMutation, or you are not following convex rules.
dannyelo
dannyeloOPβ€’3mo ago
export const getCurrentUser = async (ctx: QueryCtx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Called getCurrentUser without authentication present')
}

const user = await ctx.db
.query('users')
.withIndex('by_token', (q) =>
q.eq('tokenIdentifier', identity.tokenIdentifier),
)
.unique()

if (!user) {
throw new Error('User not found')
}

return user
}
export const getCurrentUser = async (ctx: QueryCtx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Called getCurrentUser without authentication present')
}

const user = await ctx.db
.query('users')
.withIndex('by_token', (q) =>
q.eq('tokenIdentifier', identity.tokenIdentifier),
)
.unique()

if (!user) {
throw new Error('User not found')
}

return user
}
In convex I see this error string Called getCurrentUser without authentication present
jamalsoueidan
jamalsoueidanβ€’3mo ago
yes how did you call getCurrentUser? from which function πŸ™‚
dannyelo
dannyeloOPβ€’3mo ago
From getCurrentOrganization
jamalsoueidan
jamalsoueidanβ€’3mo ago
you should NOT call other functions directly, you must use ctx.runAction, ctx.runMutation, ctx.runQuery, etc. the only time you call other functions is when you know what you are doing πŸ™‚
dannyelo
dannyeloOPβ€’3mo ago
Inside a mutation I dont have access to runAction
jamalsoueidan
jamalsoueidanβ€’3mo ago
of course, because mutation should not have sideeffects,, it should not call third party πŸ™‚ you can do it upfront in the frontend code
dannyelo
dannyeloOPβ€’3mo ago
Ah ok, I get it So, in the frontend I can call the mutation or the action conditionally?
jamalsoueidan
jamalsoueidanβ€’3mo ago
no when you call mutation its only if you need to insert something DIRECTLY without calling any third part api. if you need to call third party api, you must call "ACTION" => GETAPI > MUTATION
dannyelo
dannyeloOPβ€’3mo ago
I need to call the api conditionally, so is ok this?
dannyelo
dannyeloOPβ€’3mo ago
No description
jamalsoueidan
jamalsoueidanβ€’3mo ago
the action should not be internal...
dannyelo
dannyeloOPβ€’3mo ago
Note: In most cases calling an action directly from a client is an anti-pattern. Instead, have the client call a mutation which captures the user intent by writing into the database and then schedules an action
jamalsoueidan
jamalsoueidanβ€’3mo ago
because the frontend need to call it because you are using UseMutation in the frontend, which is incorrect const get = useAction(...);
dannyelo
dannyeloOPβ€’3mo ago
Yes, I understand the useAction, but, I have some validation, If there is the presence of some needed fields, I need to make the api call, if not, I only need to make a mutation. Thats why I'm thinking of this istead
jamalsoueidan
jamalsoueidanβ€’3mo ago
action functions can have validation args
dannyelo
dannyeloOPβ€’3mo ago
But then I see this Note: In most cases calling an action directly from a client is an anti-pattern. Instead, have the client call a mutation which captures the user intent by writing into the database and then schedules an action and I don't know if my situation if most of the cases Validation I mean in the frontend, because the api does not validate if I don't send some required fields, but, I want to save the record of the user for later update, validate and use For example if the user send me the field alias, I only save in convex, but if sends... alias, legalName, taxId, taxSystem and zip --> Then I call the api
jamalsoueidan
jamalsoueidanβ€’3mo ago
im not sure where you get those note from.
dannyelo
dannyeloOPβ€’3mo ago
No description
dannyelo
dannyeloOPβ€’3mo ago
Convex docs
dannyelo
dannyeloOPβ€’3mo ago
Actions | Convex Developer Hub
Actions can call third party services to do things such as processing a payment
jamalsoueidan
jamalsoueidanβ€’3mo ago
If you want your schedule to run as background process, then you do it like that. but you dont know in the frontend if its success or failure
Hmza
Hmzaβ€’3mo ago
@dannyelo instead of directly calling actions from mutations, you should handle third-party API calls in actions. Move your conditional API logic to an action, and have the mutation schedule this action using ctx.scheduler.runAfter. This way, you follow the correct Convex pattern: mutation > schedule action > action handles API and writes to DB. To handle conditional API calls, use an action for calling third-party APIs, not a mutation. In your case, validate the required fields in the action, and if the fields are incomplete, skip the API call and handle the database mutation separately. Avoid directly calling actions from mutationsβ€”use ctx.runAction to follow Convex patterns.
jamalsoueidan
jamalsoueidanβ€’3mo ago
@Hmza @dannyelo want the errors from the 3rd party api to the frontend, thats the whole purpose of this question. If he use schedule, he cant get the errors, if he use actions, then its possible.
dannyelo
dannyeloOPβ€’3mo ago
@Hmza Thank you, what @jamalsoueidan is saying is a good point, am I going to be able to catch errors with the scheduler method?
Hmza
Hmzaβ€’3mo ago
If you need to catch errors in real-time from the third-party API, use an action and call it directly from the frontend via useAction. The scheduler
(ctx.scheduler.runAfter)
(ctx.scheduler.runAfter)
is for background tasks and won't return immediate errors to the client. Actions will allow you to handle success or failure directly in the frontend. https://docs.convex.dev/functions/actions
Actions | Convex Developer Hub
Actions can call third party services to do things such as processing a payment
dannyelo
dannyeloOPβ€’3mo ago
@Hmza And what about calling an Action form the frontend anti-pattern?
Hmza
Hmzaβ€’3mo ago
Calling an action directly from the frontend can be considered an anti-pattern if you're not capturing the user intent or state in the database. To follow best practices, you should use a mutation to capture intent, store relevant data, and then call the action from the backend. But if your main goal is to handle third-party API calls and return errors in real-time, calling the action directly is acceptable in this case.
ballingt
ballingtβ€’3mo ago
re best practice, it's nice in case you want to set up retrying of the action (if things in the action like fetch calls are likely to fail) but if it's something that users can retry themselves and you show errors states for on the client, or you just aren't worried about retries, this works well
dannyelo
dannyeloOPβ€’3mo ago
@Hmza Ok got it, so if I want to catch errors, whether is a mutation or an action, I will have to manage the logic in the client? Something like this?
const upsertWithApi = useAction(...)
const upsertWithConvex = useMutation(...)

const onSubmit = async (data: z.infer<typeof CustomerFormSchema>) => {
try {
if (hasRequiredFields) {
upsertWithApi({ hi: 'hi' })
} else {
upsertWithConvex({ hi: 'hi' })
}
} catch (error) {
console.error('Error in onSubmit:', error)
}
}
const upsertWithApi = useAction(...)
const upsertWithConvex = useMutation(...)

const onSubmit = async (data: z.infer<typeof CustomerFormSchema>) => {
try {
if (hasRequiredFields) {
upsertWithApi({ hi: 'hi' })
} else {
upsertWithConvex({ hi: 'hi' })
}
} catch (error) {
console.error('Error in onSubmit:', error)
}
}
This way I avoid running scheduler methods
Hmza
Hmzaβ€’3mo ago
yes this will work well enough for your use case!
dannyelo
dannyeloOPβ€’3mo ago
Great! I 'll try this then! Thank you @jamalsoueidan @Hmza @ballingt πŸ™
jamalsoueidan
jamalsoueidanβ€’3mo ago
You do not use two methods for the same thing, thats incorrect. You always use one method and in the backend you handle both cases (with api, without api), and in the frontend you catch the error.
export const doSomething = action({
args: {},
handler: async (ctx, args) => {

if(needtocall3rdpartapi) {
...
}

return ctx.runMutation( // call mutation with the data from 3rd api.
},
});
export const doSomething = action({
args: {},
handler: async (ctx, args) => {

if(needtocall3rdpartapi) {
...
}

return ctx.runMutation( // call mutation with the data from 3rd api.
},
});
dannyelo
dannyeloOPβ€’3mo ago
@jamalsoueidan That also makes sense
jamalsoueidan
jamalsoueidanβ€’3mo ago
what data you getting from the 3rd party api?
dannyelo
dannyeloOPβ€’3mo ago
I'm posting data, not fetching
jamalsoueidan
jamalsoueidanβ€’3mo ago
ahh you are posting to third party ok
dannyelo
dannyeloOPβ€’3mo ago
If the post returns a success response, I know everything is ok
jamalsoueidan
jamalsoueidanβ€’3mo ago
okay perfect...then you just use action and make a condition, ctx.runAction(... and then if its success you do the mutation but do not call functions directly...
dannyelo
dannyeloOPβ€’3mo ago
Yes, I'll try that. What do you mean with: but do not call functions directly...? After I pass the logic to the Action, I'm getting these weird type errors.
export const upsertCustomerAction = action({
args: customerArgs,
handler: async (ctx, args) => {
if (!args.customerData.alias) {
throw new Error('Alias is required')
}
const organization = await ctx.runQuery(
api.organizations.getCurrentUserOrganization,
)

const hasRequiredFacturapiFields =
args.customerData.legalName &&
args.customerData.taxId &&
args.customerData.taxSystem &&
args.customerData.address?.zip

if (hasRequiredFacturapiFields) {
return await ctx.runAction(
api.customers_usenode.upsertCustomerWithFacturapiTest,
{
...args,
organizationId: organization._id,
},
)
}

return await ctx.runMutation(api.customers.upsertCustomerInConvex, {
...args,
organizationId: organization._id,
})
},
})
export const upsertCustomerAction = action({
args: customerArgs,
handler: async (ctx, args) => {
if (!args.customerData.alias) {
throw new Error('Alias is required')
}
const organization = await ctx.runQuery(
api.organizations.getCurrentUserOrganization,
)

const hasRequiredFacturapiFields =
args.customerData.legalName &&
args.customerData.taxId &&
args.customerData.taxSystem &&
args.customerData.address?.zip

if (hasRequiredFacturapiFields) {
return await ctx.runAction(
api.customers_usenode.upsertCustomerWithFacturapiTest,
{
...args,
organizationId: organization._id,
},
)
}

return await ctx.runMutation(api.customers.upsertCustomerInConvex, {
...args,
organizationId: organization._id,
})
},
})
dannyelo
dannyeloOPβ€’3mo ago
No description
dannyelo
dannyeloOPβ€’3mo ago
'upsertCustomerAction' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022) 'handler' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.ts(7023) 'organization' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022)
jamalsoueidan
jamalsoueidanβ€’3mo ago
that looks much better πŸ™‚
dannyelo
dannyeloOPβ€’3mo ago
I was returning the runAction, I remove it and solve the problem Yes!
jamalsoueidan
jamalsoueidanβ€’3mo ago
can you show me your sourcecode...so i have better overview of the whole base πŸ™‚
dannyelo
dannyeloOPβ€’3mo ago
What you do you mean of showing my source? How can I do that? You want to see the whole repository?
jamalsoueidan
jamalsoueidanβ€’3mo ago
exactly!
dannyelo
dannyeloOPβ€’3mo ago
Of course, it’s a private repo on GitHub @jamalsoueidan The implementation is working great now, thanks again!
jamalsoueidan
jamalsoueidanβ€’3mo ago
@dannyelo im glad you get it to work, remember to always use ctx.runXXX πŸ™Œ
dannyelo
dannyeloOPβ€’3mo ago
Yes I will!
ampp
amppβ€’3mo ago
I have a rather complex event management system that has several action mutation/scheduler combinations with error log/capture/display. I was using actions to capture user intent and errors but switched over to mutations in a try catch, it works most of the time but now certain valdator errors escape my front end try catch. and user intent is lost. Actions i don't recall having that issue. From my understanding one could modify the client to avoid your mutation in catch statement. So a properly written action is higher security in a way? (My justification for keeping the action code) I just havent had the time to debug the try catch nextjs.. as its like 99% of the stuff catches right.. any suggestions? hmm, It might just be simple as my error expecting a valid payload πŸ™ƒ all because i want to be able to replay the event. Is there a way to do a validation check within a mutation without throwing?
Hmza
Hmzaβ€’3mo ago
A properly written mutation can be just as secure as an action. The key is to implement proper validation and error handling. Mutations run transactionally, which can provide better data consistency guarantees. to answer your question about the validator errors escaping frontend try/catch you should use ConvexError for structured errors (throw in convex and catch it on client side) https://docs.convex.dev/functions/error-handling //mutation
if (!isValid) {
throw new ConvexError({ code: "INVALID_INPUT", message: "..." });
}
if (!isValid) {
throw new ConvexError({ code: "INVALID_INPUT", message: "..." });
}
//client
try {
await myMutation({ ... });
} catch (error) {
if (error instanceof ConvexError) {
structured errors
} else {
handle other errors
}
}
try {
await myMutation({ ... });
} catch (error) {
if (error instanceof ConvexError) {
structured errors
} else {
handle other errors
}
}
you can also handle without throwing in mutation itself
export const myMutation = mutation({
args: {},
handler: async (ctx, args) => {
//validation
const validationResult = validateArgs(args);
if (!validationResult.isValid) {
// not throwing
return { success: false, error: validationResult.error };
}
return { success: true, data:{} };
},
});
export const myMutation = mutation({
args: {},
handler: async (ctx, args) => {
//validation
const validationResult = validateArgs(args);
if (!validationResult.isValid) {
// not throwing
return { success: false, error: validationResult.error };
}
return { success: true, data:{} };
},
});
for replying event you can use a sourcing pattern that stores your raw events in db and then mutation are run on those to make sure you are processing them right. finally for your 1% problem of errors it might be worth adding more detailed logging or using a debugging tool to trace the error propagation. You could also consider using a global error boundary in React to catch any errors that escape local try/catch blocks.
Error Handling | Convex Developer Hub
There are four reasons why your Convex

Did you find this page helpful?