T
TanStack2mo ago
extended-salmon

How to handle server errors on the client side?

Hello, I am trying to learn TanStack Start and I want to show the error on the client side when an error occurs during RPC. How can I do this? Is it possible to subscribe to the states of the RPC on the server side without using a hook like useMutation? When using tRPC, we could catch errors thrown on the server side on the client side. I have this RPC:
// src/lib/functions/server.examResult.functions.ts
export const createExamResult = createServerFn({ method: "POST" })
.middleware([authMiddleware])
.validator(createExamResultSchema)
.handler(async ({ context, data }) => {
const user = context.user;

const hasExamResult = await db.query.examResult.findFirst({
where: and(eq(examResult.userId, user.id), eq(examResult.examType, data.examType)),
});

if (hasExamResult) {
throw new Error(`You already have a ${data.examType} exam result`);
}

const result = await db.insert(examResult).values({
userId: user.id,
approvalStatus: "pending",
examType: data.examType,
score: data.score,
documentBase64: data.imageBase64,
});

if (result.length === 0) {
throw new Error(`Failed to create ${data.examType} exam result`);
}

return {
success: true,
message: `${data.examType} exam result created successfully`,
};
});
// src/lib/functions/server.examResult.functions.ts
export const createExamResult = createServerFn({ method: "POST" })
.middleware([authMiddleware])
.validator(createExamResultSchema)
.handler(async ({ context, data }) => {
const user = context.user;

const hasExamResult = await db.query.examResult.findFirst({
where: and(eq(examResult.userId, user.id), eq(examResult.examType, data.examType)),
});

if (hasExamResult) {
throw new Error(`You already have a ${data.examType} exam result`);
}

const result = await db.insert(examResult).values({
userId: user.id,
approvalStatus: "pending",
examType: data.examType,
score: data.score,
documentBase64: data.imageBase64,
});

if (result.length === 0) {
throw new Error(`Failed to create ${data.examType} exam result`);
}

return {
success: true,
message: `${data.examType} exam result created successfully`,
};
});
` and I have this hook:
// src/hooks/use-create-exam.ts
export function useCreateExam() {
const router = useRouter();
const queryClient = useQueryClient();
const _createExam = useServerFn(createExamResult);

return useCallback(
async (data: CreateExamResult) => {
const result = await _createExam({ data });

router.invalidate();
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.EXAM_RESULTS });

return result;
},
[router, queryClient, _createExam],
);
}
// src/hooks/use-create-exam.ts
export function useCreateExam() {
const router = useRouter();
const queryClient = useQueryClient();
const _createExam = useServerFn(createExamResult);

return useCallback(
async (data: CreateExamResult) => {
const result = await _createExam({ data });

router.invalidate();
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.EXAM_RESULTS });

return result;
},
[router, queryClient, _createExam],
);
}
How can I handle server-side errors on the client side?
No description
2 Replies
extended-salmon
extended-salmonOP2mo ago
I found this way. Is there a better way to handle errors?
// src/hooks/use-create-exam.ts
export function useCreateExam() {
const router = useRouter();
const queryClient = useQueryClient();
const _createExam = useServerFn(createExamResult);

return useCallback(
async (data: CreateExamResult) => {
try {
const result = await _createExam({ data });

router.invalidate();
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.EXAM_RESULTS });

return result;
} catch (error) {
if (error instanceof Error) {
throw error;
}

throw new Error(`Failed to create exam: ${error}`);
}
},
[router, queryClient, _createExam],
);
}
// src/hooks/use-create-exam.ts
export function useCreateExam() {
const router = useRouter();
const queryClient = useQueryClient();
const _createExam = useServerFn(createExamResult);

return useCallback(
async (data: CreateExamResult) => {
try {
const result = await _createExam({ data });

router.invalidate();
queryClient.invalidateQueries({ queryKey: QUERY_KEYS.EXAM_RESULTS });

return result;
} catch (error) {
if (error instanceof Error) {
throw error;
}

throw new Error(`Failed to create exam: ${error}`);
}
},
[router, queryClient, _createExam],
);
}
// src/routes/dashboard/index.tsx
export const Route = createFileRoute("/dashboard/")({
component: DashboardIndex,
});

function DashboardIndex() {
const createExam = useCreateExam();

const handleCreateExam = useCallback(async () => {
toast.promise(
createExam({
examType: "kpss",
score: "100",
imageBase64: "123",
}),
{
loading: "Creating exam...",
success: (result) => result.message,
error: (error: unknown) => {
if (error instanceof Error) {
return error.message;
}

return `Failed to create exam: ${error}`;
},
},
);
}, [createExam]);

return (
<div className="flex flex-col items-center gap-1">
Dashboard index page
<pre className="bg-card text-card-foreground rounded-md border p-1">
routes/dashboard/index.tsx
</pre>
<button
className="bg-primary text-primary-foreground rounded-md p-2"
type="button"
onClick={handleCreateExam}
>
Create Exam
</button>
</div>
);
}
// src/routes/dashboard/index.tsx
export const Route = createFileRoute("/dashboard/")({
component: DashboardIndex,
});

function DashboardIndex() {
const createExam = useCreateExam();

const handleCreateExam = useCallback(async () => {
toast.promise(
createExam({
examType: "kpss",
score: "100",
imageBase64: "123",
}),
{
loading: "Creating exam...",
success: (result) => result.message,
error: (error: unknown) => {
if (error instanceof Error) {
return error.message;
}

return `Failed to create exam: ${error}`;
},
},
);
}, [createExam]);

return (
<div className="flex flex-col items-center gap-1">
Dashboard index page
<pre className="bg-card text-card-foreground rounded-md border p-1">
routes/dashboard/index.tsx
</pre>
<button
className="bg-primary text-primary-foreground rounded-md p-2"
type="button"
onClick={handleCreateExam}
>
Create Exam
</button>
</div>
);
}
wise-white
wise-white2mo ago
I'm still getting my head around Start as well but from your code above you should use a mutation instead as you are inserting a record. Then you can use the onerror callback
const formActionMutation = useMutation({
mutationFn: async (formData: PlatformUserFormType) => {
const result = await upsertPlatformUserAction({ data: formData });
return result;
},
onSuccess: async (result) => {
const targetUserId = result?.userId || userId;

// Wait for cache invalidation to complete before navigating
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["platform-users"] }),
queryClient.invalidateQueries({ queryKey: ["platform-user", targetUserId] }),
]);

router.navigate({
to: "/admin/platform/platform-users/$userId",
params: {
userId: targetUserId!,
},
});
},
onError: (error) => {
toast.error("Failed to save platform user:", error);
},
const formActionMutation = useMutation({
mutationFn: async (formData: PlatformUserFormType) => {
const result = await upsertPlatformUserAction({ data: formData });
return result;
},
onSuccess: async (result) => {
const targetUserId = result?.userId || userId;

// Wait for cache invalidation to complete before navigating
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["platform-users"] }),
queryClient.invalidateQueries({ queryKey: ["platform-user", targetUserId] }),
]);

router.navigate({
to: "/admin/platform/platform-users/$userId",
params: {
userId: targetUserId!,
},
});
},
onError: (error) => {
toast.error("Failed to save platform user:", error);
},

Did you find this page helpful?