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;
}
}
// Usage
const 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(),
});

Frequently Asked Questions

Should I capture validation errors?
Generally no. Validation errors are expected and user-facing. Capture unexpected errors like database failures, third-party API errors, or logic errors that shouldn't happen.
How do I avoid capturing the same error multiple times?
Capture at the highest level where you handle the error. If you rethrow an error, don't capture it at the lower level. Let it bubble up to where it's finally handled.
What's the difference between captureException and captureEvent?
captureException is for errors and exceptions. captureEvent is for tracking important actions or milestones that aren't errors, like 'user_deleted_account' or 'large_export_started'.
Does capturing errors affect performance?
Minimally. Error capturing is asynchronous and non-blocking. However, avoid capturing in hot paths or loops. Capture at boundaries, not in every function.

Next: Creating a Custom Monitoring Provider →