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:
components/error-boundary.tsx
import { useCaptureException } from '@kit/monitoring/hooks';export default function ErrorPage({ error, reset,}: { error: Error & { digest?: string }; reset: () => void;}) { // Errors with a `digest` originated server-side and were already captured // by `onRequestError`. Pass `null` to skip the client report and avoid a // duplicate event in your dashboard. useCaptureException(error.digest ? null : error); return ( <div> <h2>Something went wrong</h2> <button onClick={reset}>Try again</button> </div> );}This hook captures the error once when the component mounts. It's ideal for error boundary pages. Pass null whenever you want to skip the report.
Capturing outside the MonitoringProvider tree
Some client code runs outside the MonitoringProvider — for example a top-level fallback that renders when the provider tree itself failed to mount — so the useMonitoring / useCaptureException hooks can't reach the context. Use captureClientException from @kit/monitoring/instrumentation-client instead — it talks to the same lazily-loaded provider without needing the React tree:
apps/web/src/components/root-error-fallback.tsx
import { useEffect } from 'react';import { captureClientException } from '@kit/monitoring/instrumentation-client';export function RootErrorFallback({ error,}: { error: Error & { digest?: string };}) { useEffect(() => { if (error.digest) return; void captureClientException(error); }, [error]); return <h2>Something went wrong</h2>;}Using the useMonitoring Hook
For more control, use the useMonitoring hook to access the monitoring service directly:
components/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
lib/server/create-project.functions.ts
import { createServerFn } from '@tanstack/react-start';import { getServerMonitoringService } from '@kit/monitoring/server';import { CreateProjectSchema } from './create-project.schema';export const createProjectFunction = createServerFn({ method: 'POST' }) .validator(CreateProjectSchema) .handler(async ({ data }) => { const monitoring = await getServerMonitoringService(); await monitoring.ready(); try { const project = await db.project.create({ data: { name: data.name }, }); return { success: true, project }; } catch (error) { await monitoring.captureException(error as Error, { action: 'createProject', name: data.name, }); return { success: false, error: 'Failed to create project' }; } });In Server Routes
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 }) => { 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' }, { url: request.url, method: request.method, }, ); 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: Sentry-specific config
monitoring.captureException( error, { userId: user.id }, { // Sentry-specific options 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 redirect('/login'); } // 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(),});