TanStack Start is a full-stack React framework that lets you write server and client code in the same file, breaking down the traditional API layer. But this convenience introduces complexity in error handling—especially when errors cross the RPC boundary between server functions and client code. Getting TanStack Start error handling monitoring right means catching errors at three critical points: in server functions (which run on your backend), during RPC communication, and at the route level in React. Without proper instrumentation, errors silently fail or surface only as generic network failures, making debugging in production nearly impossible.
This guide walks you through capturing every error in your TanStack Start app using the Sentry SDK (which works seamlessly with LightTrace), organizing errors by source, and building a mental model of the full stack. You'll learn how to instrument server functions, handle RPC errors gracefully, and set up error boundaries that don't swallow crucial debugging information.
Why Full-Stack Error Handling Requires a New Approach
TanStack Start collapses the traditional client-server boundary. Server functions live in server$() blocks and are called directly from client code—no explicit HTTP routes, no obvious error boundaries. This architecture is powerful but means a single logical operation spans two runtimes.
Traditional error tracking often assumes a clean separation: client errors go to the browser, server errors hit logs on the backend, and the two never meet. In TanStack Start, you need a unified view. When a server function throws, the error bubbles through the RPC transport; when the client calls that function incorrectly, it fails on the client side. Both scenarios deserve monitoring, but they require different instrumentation patterns.
If you're unfamiliar with error tracking basics, start with what error tracking is and why it matters before diving into framework-specific setup.
Setting Up Sentry SDK for LightTrace
The Sentry SDK works with LightTrace—just point the DSN to your LightTrace instance. For TanStack Start, you'll instrument both client and server entry points.
Client setup (src/entry-client.tsx or similar):
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
integrations: [
new Sentry.Replay(),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1,
});
Server setup (src/entry-server.ts or similar):
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
Sentry.init({
dsn: "https://<your-key>@light-trace.robomiri.com/1",
integrations: [nodeProfilingIntegration()],
tracesSampleRate: 1.0,
});
The key difference: the server samples at 100% (all transactions) to catch rare errors; the client samples lower to avoid overwhelming your quota. Both send errors to the same LightTrace project, so you get a unified view of failures across the full stack.
Use a lower sample rate on the client only if you have extremely high traffic. For most apps, sampling both at 100% during development and 10–50% in production is the safe default.
RPC Error Propagation and Handling
When you call a server$() function from the client, TanStack Start wraps it in an RPC call. If the function throws, the error serializes and crosses the network back to the client. On the client, the promise rejects with that error.
// server$() function
const addUser = server$(async (name: string) => {
if (!name || name.length < 2) {
throw new Error("Name must be at least 2 characters");
}
const user = await db.users.create({ name });
return user;
});
// Client code calling it
export function UserForm() {
const [error, setError] = useState<Error | null>(null);
const handleSubmit = async (name: string) => {
try {
const user = await addUser(name);
// handle success
} catch (err) {
setError(err as Error);
// Sentry captures this automatically
}
};
// ...
}
The Sentry SDK will automatically capture uncaught promise rejections and exceptions. But you can also manually report RPC errors with context:
import * as Sentry from "@sentry/browser";
const handleSubmit = async (name: string) => {
try {
const user = await addUser(name);
} catch (err) {
Sentry.captureException(err, {
tags: { component: "UserForm", operation: "addUser" },
level: "error",
});
setError(err as Error);
}
};
By tagging RPC errors, you can group them in LightTrace and spot patterns—for example, seeing that addUser fails in a specific browser or for users in a region where your validation is too strict.
RPC errors that serialize across the network lose their stack trace. The server's call stack doesn't travel with the error. Instead, capture context on the server before throwing, using Sentry's breadcrumb API, so you preserve the debugging trail.
Server Function Error Tracking
Server functions run in Node.js, so you can attach context and breadcrumbs before errors escape. Use Sentry's withScope to tie data to errors:
import * as Sentry from "@sentry/node";
const addUser = server$(async (name: string) => {
return Sentry.withScope(async (scope) => {
scope.setTag("operation", "createUser");
scope.setContext("input", { name });
try {
// Simulate a database call
const user = await db.users.create({ name });
return user;
} catch (err) {
// Explicitly report with context
Sentry.captureException(err, {
level: "error",
tags: { service: "database" },
});
throw err; // Re-throw so client knows it failed
}
});
});
When the error propagates to the client, the RPC transport loses the original stack trace—but all the context you attached on the server stays in LightTrace. You see exactly which operation failed, what inputs triggered it, and which service (database, API, etc.) threw the error.
For async server functions that spawn background work, use Sentry.captureException synchronously before the promise settles:
const processPayment = server$(async (orderId: string) => {
Sentry.setTag("orderId", orderId);
const result = await paymentGateway.charge(orderId);
// Fire and forget cleanup, but capture errors
cleanup(orderId).catch((err) => {
Sentry.captureException(err, { tags: { background: true, orderId } });
});
return result;
});
This ensures background errors don't disappear; they land in LightTrace tagged so you know they happened asynchronously.
Route-Level Error Boundaries
TanStack Start uses React's error boundary pattern. When rendering fails, an error boundary catches it and prevents the entire app from crashing. But error boundaries also need to report—otherwise you'll never know an error happened unless a user files a bug report.
import * as Sentry from "@sentry/react";
import { ErrorBoundary } from "react-error-boundary";
function ErrorFallback({ error, resetErrorBoundary }: any) {
return (
<div>
<h2>Something went wrong.</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function MyRoute() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
// Sentry captures this
Sentry.captureException(error, {
contexts: { react: info },
tags: { route: "my-route" },
});
}}
onReset={() => {
// User clicked retry
}}
>
<YourComponent />
</ErrorBoundary>
);
}
The Sentry @sentry/react package includes a Sentry-wrapped error boundary integration, but explicitly calling captureException in the onError handler ensures you tag errors by route and preserve the component stack.
Error boundaries only catch render-time errors, not errors in event handlers or async code. For JavaScript error tracking, also set up a global error handler: window.addEventListener("error", (event) => { Sentry.captureException(event.error); }).
Breadcrumbs and Replay for Context
Errors without context are nearly impossible to debug. Use breadcrumbs to log user actions leading up to an error, and enable replay to see a video of what happened.
import * as Sentry from "@sentry/browser";
function UserForm() {
const handleChange = (value: string) => {
Sentry.captureMessage("User typed in form", {
level: "debug",
contexts: { form: { field: "username", value } },
});
// ... update state
};
const handleSubmit = async (data: any) => {
Sentry.addBreadcrumb({
message: "Submitting user form",
data: { fields: Object.keys(data) },
level: "info",
});
try {
await addUser(data);
} catch (err) {
// Error includes the breadcrumbs automatically
Sentry.captureException(err);
}
};
// ...
}
In LightTrace, these breadcrumbs appear in the error detail view—you see exactly which form fields the user touched and in what order, making reproduction simple.
Best Practices for TanStack Start Error Handling
Distinguish server from client errors. Tag all server-originating errors with a source: "server" tag so you can filter them separately in LightTrace. Similarly, tag client errors with source: "client". This separation helps you route errors to the right team (backend team gets server errors, frontend team gets client errors).
Avoid throwing generic errors. Instead of throw new Error("Something failed"), include context:
throw new Error(`Failed to create user: ${db.error.code} - ${db.error.message}`);
Sample heavy operations. If your app has millions of events per month, sample performance transactions on the server (via tracesSampleRate). But capture all errors—never sample errors out. Adjust via LightTrace's Data Management settings.
Monitor your error monitor. If Sentry SDK initialization fails or the DSN is wrong, errors won't reach LightTrace. Test this in staging. Also check your LightTrace quota and alert rules so you're not surprised when errors stop appearing.
For deeper guidance on error tracking architecture, read about adding error tracking to your app.
Shipping Errors Safely to Production
Once your app is instrumented, enable error alerting in LightTrace. Set up alert rules for new issues and error frequency spikes. By default, LightTrace sends alerts via email—set a rule to notify you when errors from production exceed a threshold, like more than 10 events per hour.
Test error reporting in staging by deliberately triggering errors and verifying they appear in LightTrace within a few seconds. Confirm that breadcrumbs, tags, and context are all present. Only then ship to production.
Use LightTrace's "Releases" feature to associate errors with a specific app version. When you deploy, set release in the Sentry config: Sentry.init({ release: "1.0.0", ... }). Now you can instantly see which release introduced an error.
TanStack Start's full-stack design means errors hide in multiple places. By instrumenting the RPC boundary, server functions, and route-level boundaries, you turn a blind spot into a fully visible system. Your error tracking becomes a first-class part of the developer experience, not a firefighting tool.
Start tracking errors in minutes
Catch errors in your TanStack Start app in real time. Start free with LightTrace — no credit card required.