justindgm
justindgm13mo ago

Vanilla Client Error Handling

What is the right way to handle errors when using the vanilla client? If I setup a client like so:
import type { Router } from '../../../server/src/routers'

export const trpcClient = createTRPCProxyClient<Router>({
links: [
httpBatchLink({
url: `${env.SERVER_URI}/trpc`
})
]
})
import type { Router } from '../../../server/src/routers'

export const trpcClient = createTRPCProxyClient<Router>({
links: [
httpBatchLink({
url: `${env.SERVER_URI}/trpc`
})
]
})
And execute a mutation like so:
const user = await trpcClient.user.create.mutate({ email: string; password: string })
const user = await trpcClient.user.create.mutate({ email: string; password: string })
How should I handle errors? If I try to directly catch when calling the mutate function, the error is typed as any
const user = await trpcClient.user.create.mutate({ email: string; password: string }).catch((error) => {
// error is any
}
const user = await trpcClient.user.create.mutate({ email: string; password: string }).catch((error) => {
// error is any
}
Is there a way to type narrow the error at runtime? On the server side, I am using the errorFormatter to add a custom field to my errors:
const t = initTRPC.context<Context>().create({
isDev: env.NODE_ENV !== 'production',
errorFormatter: ({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
customError: error.cause instanceof CustomError ? error.cause.customCode : null
}
}
}
})
const t = initTRPC.context<Context>().create({
isDev: env.NODE_ENV !== 'production',
errorFormatter: ({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
customError: error.cause instanceof CustomError ? error.cause.customCode : null
}
}
}
})
My current approach will be to use a runtime validation library like zod to check if this custom field exists on the error
const user = await trpcClient.user.create.mutate({ email: string; password: string }).catch((error) => {
const parsedError = errorSchema.safeParse(error)
if (parsedError.isSuccess) {
console.error(parsedError.data.data.customError)
} else {
console.error(error)
}
}
const user = await trpcClient.user.create.mutate({ email: string; password: string }).catch((error) => {
const parsedError = errorSchema.safeParse(error)
if (parsedError.isSuccess) {
console.error(parsedError.data.data.customError)
} else {
console.error(error)
}
}
But this is clearly not ideal. Is this the right/best way to approach error handling with the vanilla client, or is there a better approach?
Solution:
There is actually a docs page on this, does that answer the question? https://trpc.io/docs/client/vanilla/infer-types#infer-trpcclienterror-types
Inferring Types | tRPC
It is often useful to access the types of your API within your clients. For this purpose, you are able to infer the types contained in your AppRouter.
Jump to solution
6 Replies
Krishna
Krishna13mo ago
I would also like to understand this... I also want to handle error centrally... for example if backend gives an unauthorised error (maybe due to token expiry)... Need to logout the user from frontend & bring the user back to their existing state once they login....
justindgm
justindgmOP13mo ago
@Krishna Agreed, global handling of certain errors (especially authentication/token expiry), is exactly what my next question would be once I understand how to best handle errors in the most basic case.
Solution
Nick
Nick12mo ago
There is actually a docs page on this, does that answer the question? https://trpc.io/docs/client/vanilla/infer-types#infer-trpcclienterror-types
Inferring Types | tRPC
It is often useful to access the types of your API within your clients. For this purpose, you are able to infer the types contained in your AppRouter.
LittleLily
LittleLily12mo ago
also interested in knowing what the best solution for globally handling errors when using createTRPCProxyClient is. I found this discussion https://github.com/trpc/trpc/discussions/2036 but none of the solutions there seem to really be what I want. I'd like to be able to return custom responses to the client like 302 redirects on auth failures and such, but it seems like only createTRPCNext really supports that with its responseMeta field.
justindgm
justindgmOP12mo ago
This is perfect! Thank you
BillyBob
BillyBob11mo ago
@Krishna @justindgm How do you handle errors centrally ?
import { redirect } from 'next/navigation'

export const customLink: TRPCLink<AppRouter> = () => {
return ({ next, op }) => {
return observable((observer) => {
const unsubscribe = next(op).subscribe({
next(value) {
observer.next(value)
},
error(err) {
observer.error(err)
if (err?.data?.code === 'UNAUTHORIZED') {
redirect('/login')
}
},
complete() {
observer.complete()
},
})
return unsubscribe
})
}
}

export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink({
// enabled: (op) => false,
// // enabled: (op) =>
// // process.env.NODE_ENV === 'development' ||
// // (op.direction === 'down' && op.result instanceof Error),
// }),
customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers: Object.fromEntries(headers()),
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
})
},
}),
],
})
import { redirect } from 'next/navigation'

export const customLink: TRPCLink<AppRouter> = () => {
return ({ next, op }) => {
return observable((observer) => {
const unsubscribe = next(op).subscribe({
next(value) {
observer.next(value)
},
error(err) {
observer.error(err)
if (err?.data?.code === 'UNAUTHORIZED') {
redirect('/login')
}
},
complete() {
observer.complete()
},
})
return unsubscribe
})
}
}

export const api = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
// loggerLink({
// enabled: (op) => false,
// // enabled: (op) =>
// // process.env.NODE_ENV === 'development' ||
// // (op.direction === 'down' && op.result instanceof Error),
// }),
customLink,
httpBatchLink({
url: 'http://localhost:4000/trpc',
headers: Object.fromEntries(headers()),
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
})
},
}),
],
})
Ive been trying this but the redirectdoes not seem to run