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

'use client';
import { useCaptureException } from '@kit/monitoring/hooks';
export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
// Automatically captures the error when the component mounts
useCaptureException(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.

Using the useMonitoring Hook

For more control, use the useMonitoring hook to access the monitoring service directly:

components/form.tsx

'use client';
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

'use client';
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 Actions

lib/actions/create-project.ts

'use server';
import { getServerMonitoringService } from '@kit/monitoring/server';
export async function createProject(formData: FormData) {
const monitoring = await getServerMonitoringService();
await monitoring.ready();
try {
const project = await db.project.create({
data: {
name: formData.get('name') as string,
},
});
return { success: true, project };
} catch (error) {
await monitoring.captureException(error as Error, {
action: 'createProject',
formData: Object.fromEntries(formData),
});
return { success: false, error: 'Failed to create project' };
}
}

In API Routes

app/api/webhook/route.ts

import { getServerMonitoringService } from '@kit/monitoring/server';
export async function POST(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: 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

'use client';
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 →