# Manual Error Capturing

> Learn how to manually capture errors and exceptions in both client and server code using Makerkit monitoring hooks and services.

*Canonical: https://makerkit.dev/docs/tanstack-drizzle/monitoring/capturing-errors*

---

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.

{% sequence title="Error Capturing Guide" description="Manually capture errors in your application" %}

[Client-Side Error Capturing](#client-side-error-capturing)

[Server-Side Error Capturing](#server-side-error-capturing)

[Adding Context to Errors](#adding-context-to-errors)

[Identifying Users](#identifying-users)

{% /sequence %}

## 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`:

```tsx {% title="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.

{% alert type="info" title="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.
{% /alert %}

### 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:

```tsx {% title="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:

```tsx {% title="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`:

```tsx {% title="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

```typescript {% title="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:

```typescript {% title="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:

```typescript
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:

```typescript {% title="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:

```typescript {% title="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:

```typescript {% title="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

```typescript {% title="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:

```typescript {% title="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:

```typescript {% title="Good context" %}
monitoring.captureException(error, {
  userId: user.id,
  action: 'export_csv',
  rowCount: data.length,
  filters: appliedFilters,
  exportFormat: 'csv',
});
```

```typescript {% title="Less useful context" %}
monitoring.captureException(error, {
  error: 'something went wrong',
  time: Date.now(),
});
```

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "Should I capture validation errors?", "answer": "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."},
     {"question": "How do I avoid capturing the same error multiple times?", "answer": "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."},
     {"question": "What's the difference between captureException and captureEvent?", "answer": "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'."},
     {"question": "Does capturing errors affect performance?", "answer": "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 →](./custom-monitoring-provider)
