Manual Error Capturing
Learn how to manually capture errors and exceptions in both client and server code using Makerkit monitoring hooks and services.
While Makerkit automatically captures unhandled errors, you often need to capture errors manually in try-catch blocks, form submissions, or API calls. This guide shows you how to capture errors programmatically.
Error Capturing Guide
Manually capture errors in your application
Client-Side Error Capturing
Using the useCaptureException Hook
The simplest way to capture errors in React components is the useCaptureException hook. Use it from a TanStack Router errorComponent:
apps/web/src/components/error-boundary.tsx
import type { ErrorComponentProps } from '@tanstack/react-router';import { useCaptureException } from '@kit/monitoring/hooks';export function ErrorBoundary({ error }: ErrorComponentProps) { // Pass `null` to skip capturing. Errors that originated server-side were // already captured by the provider's `onRequestError` instrumentation, so // re-capturing them on the client would double-report. The kit tags such // errors with a `digest`. const digest = (error as Error & { digest?: string }).digest; useCaptureException(digest ? null : error); return ( <div> <h2>Something went wrong</h2> </div> );}This hook captures the error once when the component mounts. It's ideal for route error components.
Dedup with server errors
Errors that originated server-side are already captured by the provider's onRequestError instrumentation. The kit marks such errors with a digest, so a route errorComponent passes null to useCaptureException when a digest is present to avoid double-reporting.
Capturing Outside the Provider Tree
A root-level error boundary may render outside the React MonitoringContext (no useMonitoring available). Use captureClientException for these cases — it lazily loads the configured provider and forwards the error:
apps/web/src/components/global-error.tsx
import { useEffect } from 'react';import { captureClientException } from '@kit/monitoring/instrumentation-client';export function GlobalError({ error }: { error: Error }) { const digest = (error as Error & { digest?: string }).digest; useEffect(() => { if (!digest) { void captureClientException(error); } }, [error, digest]); return <div>Something went wrong</div>;}Using the useMonitoring Hook
For more control, use the useMonitoring hook to access the monitoring service directly:
apps/web/src/components/contact-form.tsx
import { useMonitoring } from '@kit/monitoring/hooks';export function ContactForm() { const monitoring = useMonitoring(); const handleSubmit = async (formData: FormData) => { try { await submitForm(formData); } catch (error) { // Capture the error with context monitoring.captureException(error as Error, { formData: Object.fromEntries(formData), action: 'contact_form_submit', }); // Show user-friendly error toast.error('Failed to submit form'); } }; return ( <form action={handleSubmit}> {/* form fields */} </form> );}Capturing Events
Track custom monitoring events (not errors) using captureEvent:
Example: Tracking important actions
import { useMonitoring } from '@kit/monitoring/hooks';export function DangerZone() { const monitoring = useMonitoring(); const handleDeleteAccount = async () => { // Track the action before attempting monitoring.captureEvent('account_deletion_attempted', { userId: user.id, timestamp: new Date().toISOString(), }); try { await deleteAccount(); } catch (error) { monitoring.captureException(error as Error); } }; return ( <button onClick={handleDeleteAccount}> Delete Account </button> );}Server-Side Error Capturing
In Server Functions
packages/projects/src/functions/projects.functions.ts
import { createServerFn } from '@tanstack/react-start';import { organizationFunctionMiddleware } from '@kit/function-middleware/functions';import { getServerMonitoringService } from '@kit/monitoring/server';export const createProjectFunction = createServerFn({ method: 'POST' }) .middleware(organizationFunctionMiddleware) .validator(CreateProjectSchema) .handler(async ({ data, context }) => { const monitoring = await getServerMonitoringService(); await monitoring.ready(); try { return await createProject({ organizationId: context.organizationId, name: data.name, }); } catch (error) { await monitoring.captureException(error as Error, { action: 'createProject', name: data.name, }); throw error; } });In API Route Handlers
TanStack Start API routes live under apps/web/src/routes/api/* and expose raw Request/Response handlers:
apps/web/src/routes/api/webhook.ts
import { createFileRoute } from '@tanstack/react-router';import { getServerMonitoringService } from '@kit/monitoring/server';export const Route = createFileRoute('/api/webhook')({ server: { handlers: { POST: async ({ request }: { request: Request }) => { const monitoring = await getServerMonitoringService(); await monitoring.ready(); try { const data = await request.json(); await processWebhook(data); return Response.json({ received: true }); } catch (error) { await monitoring.captureException( error as Error, { webhook: 'stripe' }, { path: request.url, method: 'POST', } ); return Response.json( { error: 'Webhook processing failed' }, { status: 500 } ); } }, }, },});Adding Context to Errors
The captureException method accepts two optional parameters for adding context:
captureException( error: Error, extra?: Record<string, unknown>, // Additional data config?: Record<string, unknown> // Provider-specific config)Extra Data
Add any relevant information that helps debug the error:
Example: Rich error context
monitoring.captureException(error, { // User context userId: user.id, userEmail: user.email, userPlan: user.subscription.plan, // Action context action: 'checkout', productId: product.id, quantity: cart.items.length, // Environment context feature_flags: getEnabledFlags(), app_version: process.env.APP_VERSION,});Configuration
Pass provider-specific configuration:
Example: provider-specific config
monitoring.captureException( error, { userId: user.id }, { // provider-specific options (depends on your registered provider) level: 'error', tags: { component: 'checkout', flow: 'payment', }, });Identifying Users
Associate errors with users to track issues per user:
Example: Identify user after login
import { useMonitoring } from '@kit/monitoring/hooks';export function useIdentifyUser(user: { id: string; email: string }) { const monitoring = useMonitoring(); useEffect(() => { if (user) { monitoring.identifyUser({ id: user.id, email: user.email, }); } }, [user, monitoring]);}Once identified, all subsequent errors from that session are associated with the user in your monitoring dashboard.
Best Practices
Capture at Boundaries
Capture errors at the boundaries of your application:
- Form submissions
- API calls
- Third-party integrations
- File uploads
- Payment processing
Pattern: Error boundary function
async function withErrorCapture<T>( fn: () => Promise<T>, context: Record<string, unknown>): Promise<T | null> { const monitoring = await getServerMonitoringService(); await monitoring.ready(); try { return await fn(); } catch (error) { await monitoring.captureException(error as Error, context); return null; }}// Usageconst result = await withErrorCapture( () => processPayment(paymentData), { action: 'processPayment', amount: paymentData.amount });Don't Over-Capture
Not every error needs to be captured:
Example: Selective capturing
try { await fetchUserData();} catch (error) { if (error instanceof NetworkError) { // Expected error, handle gracefully return fallbackData; } if (error instanceof AuthError) { // Expected error, redirect to login (e.g. throw redirect({ to: '/auth/sign-in' })) throw redirect({ to: '/auth/sign-in' }); } // Unexpected error, capture it monitoring.captureException(error as Error); throw error;}Include Actionable Context
Add context that helps you fix the issue:
Good context
monitoring.captureException(error, { userId: user.id, action: 'export_csv', rowCount: data.length, filters: appliedFilters, exportFormat: 'csv',});Less useful context
monitoring.captureException(error, { error: 'something went wrong', time: Date.now(),});