justindgm
justindgm15mo 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
8 Replies
Krishna
Krishna15mo 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
justindgmOP15mo 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
Nick15mo 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
LittleLily15mo 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
justindgmOP15mo ago
This is perfect! Thank you
BillyBob
BillyBob13mo 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
Nextor2k
Nextor2k2w ago
@BillyBob Maybe its too late, was trying to sort it out, best way I could do it was the following:
import { TRPCClientError } from '@trpc/client';
import { httpBatchLink, createTRPCProxyClient } from '@trpc/client';
import type { AppRouter } from '@poseidon/proto-onboard-server/trpc';
import { getServerUrl } from './utils';
import { redirect } from 'next/navigation';
import { getSession } from './auth';

export function isTRPCClientError(
cause: unknown,
): cause is TRPCClientError<AppRouter> {
return cause instanceof TRPCClientError;
}

export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: `${getServerUrl()}/trpc`,
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
});
},
async headers() {
const session = await getSession();
if (session?.user.accessToken) {
return {
Authorization: `Bearer ${session.user.accessToken}`,
};
}
return {};
},
}),
],
});

export const withClient = <T>(fn: (api: typeof trpc) => Promise<T>) => {
return fn(trpc).catch((cause: unknown) => {
if (isTRPCClientError(cause)) {
if (cause.data?.code === 'UNAUTHORIZED') {
redirect('/en/overview/session-expired');
}
}
throw cause;
});
};
import { TRPCClientError } from '@trpc/client';
import { httpBatchLink, createTRPCProxyClient } from '@trpc/client';
import type { AppRouter } from '@poseidon/proto-onboard-server/trpc';
import { getServerUrl } from './utils';
import { redirect } from 'next/navigation';
import { getSession } from './auth';

export function isTRPCClientError(
cause: unknown,
): cause is TRPCClientError<AppRouter> {
return cause instanceof TRPCClientError;
}

export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: `${getServerUrl()}/trpc`,
fetch(url, options) {
return fetch(url, {
...options,
credentials: 'include',
});
},
async headers() {
const session = await getSession();
if (session?.user.accessToken) {
return {
Authorization: `Bearer ${session.user.accessToken}`,
};
}
return {};
},
}),
],
});

export const withClient = <T>(fn: (api: typeof trpc) => Promise<T>) => {
return fn(trpc).catch((cause: unknown) => {
if (isTRPCClientError(cause)) {
if (cause.data?.code === 'UNAUTHORIZED') {
redirect('/en/overview/session-expired');
}
}
throw cause;
});
};
then you can use like this:
const result = await withClient((api) => api.auth.session.query());
const result = await withClient((api) => api.auth.session.query());
is there a better way?
BillyBob
BillyBob2w ago
Can’t remember what I did now but can share if anything decent 👍🏼

Did you find this page helpful?