Transactional

Capturing Errors

Learn how to capture errors automatically and manually in your applications.

Overview

There are two ways to capture errors:

  1. Automatic Capture - SDK captures uncaught exceptions and unhandled rejections
  2. Manual Capture - You explicitly capture errors in your code

Automatic Capture

Enable automatic error capture during initialization:

import { initObservability } from '@transactional/observability';
 
initObservability({
  dsn: 'your-dsn',
  enableErrorTracking: true,
 
  autoCapture: {
    // Capture uncaught exceptions (default: true)
    uncaughtExceptions: true,
 
    // Capture unhandled promise rejections (default: true)
    unhandledRejections: true,
 
    // Capture console.error calls (default: false)
    consoleErrors: false,
  },
});

What Gets Captured Automatically

EventCaptured By Default
throw new Error() (uncaught)Yes
Unhandled Promise rejectionYes
window.onerror (browser)Yes
process.on('uncaughtException') (Node)Yes
console.error()No (opt-in)

Manual Capture

captureException()

Capture caught exceptions:

import { getObservability } from '@transactional/observability';
 
const obs = getObservability();
 
try {
  await riskyOperation();
} catch (error) {
  // Capture the error
  obs.captureException(error as Error);
 
  // Handle gracefully
  showErrorToUser();
}

With Additional Context

Add context to help with debugging:

obs.captureException(error as Error, {
  // Custom tags for filtering
  tags: {
    feature: 'checkout',
    paymentProvider: 'stripe',
  },
 
  // Extra debugging data
  extra: {
    orderId: order.id,
    cartItems: cart.items.length,
    totalAmount: cart.total,
  },
 
  // User who experienced the error
  user: {
    id: user.id,
    email: user.email,
  },
 
  // Override severity
  severity: 'warning',
 
  // Custom fingerprint for grouping
  fingerprint: ['checkout-error', error.code],
});

captureMessage()

Capture non-exception error messages:

// Simple message
obs.captureMessage('User attempted checkout with empty cart');
 
// With severity
obs.captureMessage('Payment retry succeeded after 3 attempts', 'warning');
 
// With full context
obs.captureMessage('Rate limit exceeded for user', 'warning', {
  tags: { endpoint: '/api/generate' },
  extra: { userId: user.id, requestsToday: 150 },
});

React Error Boundaries

Capture errors in React component trees:

Using the Built-in ErrorBoundary

import { ErrorBoundary } from '@transactional/observability/react';
 
function App() {
  return (
    <ErrorBoundary
      fallback={<ErrorPage />}
      onError={(error, componentStack) => {
        console.error('React error:', error);
      }}
    >
      <YourApp />
    </ErrorBoundary>
  );
}

Custom Error Boundary

import { Component, ReactNode } from 'react';
import { getObservability } from '@transactional/observability';
 
interface Props {
  children: ReactNode;
  fallback: ReactNode;
}
 
interface State {
  hasError: boolean;
}
 
class CustomErrorBoundary extends Component<Props, State> {
  state = { hasError: false };
 
  static getDerivedStateFromError(): State {
    return { hasError: true };
  }
 
  componentDidCatch(error: Error, errorInfo: { componentStack: string }) {
    const obs = getObservability();
 
    obs.captureException(error, {
      extra: {
        componentStack: errorInfo.componentStack,
      },
      tags: {
        errorBoundary: 'custom',
      },
    });
  }
 
  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

Granular Error Boundaries

Wrap specific features for better isolation:

function Dashboard() {
  return (
    <div>
      <Header />
 
      <ErrorBoundary fallback={<ChartError />}>
        <AnalyticsChart />
      </ErrorBoundary>
 
      <ErrorBoundary fallback={<TableError />}>
        <DataTable />
      </ErrorBoundary>
    </div>
  );
}

Express/Hono Middleware

Express

import express from 'express';
import { getObservability } from '@transactional/observability';
 
const app = express();
 
// Your routes
app.get('/api/users', async (req, res) => {
  // ...
});
 
// Error handling middleware (must be last)
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
  const obs = getObservability();
 
  obs.captureException(err, {
    tags: {
      route: req.path,
      method: req.method,
    },
    extra: {
      query: req.query,
      body: req.body,
    },
    user: req.user ? { id: req.user.id } : undefined,
  });
 
  res.status(500).json({ error: 'Internal Server Error' });
});

Hono

import { Hono } from 'hono';
import { getObservability } from '@transactional/observability';
 
const app = new Hono();
 
// Error handler
app.onError((err, c) => {
  const obs = getObservability();
 
  obs.captureException(err, {
    tags: {
      route: c.req.path,
      method: c.req.method,
    },
    request: {
      url: c.req.url,
      method: c.req.method,
      headers: Object.fromEntries(c.req.raw.headers),
    },
  });
 
  return c.json({ error: 'Internal Server Error' }, 500);
});

Next.js Integration

App Router - Error Component

// app/error.tsx
'use client';
 
import { useEffect } from 'react';
import { getObservability } from '@transactional/observability';
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    const obs = getObservability();
    obs.captureException(error, {
      tags: { nextjs: 'app-router', boundary: 'error.tsx' },
      extra: { digest: error.digest },
    });
  }, [error]);
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

App Router - Global Error

// app/global-error.tsx
'use client';
 
import { useEffect } from 'react';
import { getObservability } from '@transactional/observability';
 
export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    const obs = getObservability();
    obs.captureException(error, {
      tags: { nextjs: 'app-router', boundary: 'global-error.tsx' },
      severity: 'fatal',
    });
  }, [error]);
 
  return (
    <html>
      <body>
        <h2>Something went wrong!</h2>
        <button onClick={reset}>Try again</button>
      </body>
    </html>
  );
}

API Routes

// app/api/users/route.ts
import { NextResponse } from 'next/server';
import { getObservability } from '@transactional/observability';
 
export async function GET(request: Request) {
  const obs = getObservability();
 
  try {
    const users = await fetchUsers();
    return NextResponse.json(users);
  } catch (error) {
    obs.captureException(error as Error, {
      tags: { route: '/api/users', method: 'GET' },
    });
 
    return NextResponse.json(
      { error: 'Failed to fetch users' },
      { status: 500 }
    );
  }
}

Async Error Handling

Async/Await

async function processOrder(orderId: string) {
  const obs = getObservability();
 
  try {
    const order = await fetchOrder(orderId);
    await chargePayment(order);
    await fulfillOrder(order);
  } catch (error) {
    obs.captureException(error as Error, {
      tags: { operation: 'process-order' },
      extra: { orderId },
    });
    throw error; // Re-throw if needed
  }
}

Promise Chains

fetchOrder(orderId)
  .then(chargePayment)
  .then(fulfillOrder)
  .catch((error) => {
    obs.captureException(error, {
      tags: { operation: 'process-order' },
      extra: { orderId },
    });
  });

Filtering Errors

Before Send Hook

Filter or modify errors before they're sent:

initObservability({
  dsn: 'your-dsn',
  enableErrorTracking: true,
 
  beforeSend: (event) => {
    // Don't send errors from development
    if (event.environment === 'development') {
      return null;
    }
 
    // Don't send network errors
    if (event.exceptionType === 'NetworkError') {
      return null;
    }
 
    // Scrub sensitive data
    if (event.request?.headers) {
      delete event.request.headers['authorization'];
    }
 
    return event;
  },
});

Ignore Errors

Ignore specific error types:

initObservability({
  dsn: 'your-dsn',
  enableErrorTracking: true,
 
  ignoreErrors: [
    // Ignore by message pattern
    /ResizeObserver loop/,
    /Network request failed/,
 
    // Ignore by exception type
    'AbortError',
    'CancelledError',
  ],
});

Sampling

Control how many errors are captured:

initObservability({
  dsn: 'your-dsn',
  enableErrorTracking: true,
 
  // Capture 50% of errors (for high-volume apps)
  sampleRate: 0.5,
 
  // Or use a function for dynamic sampling
  tracesSampler: (context) => {
    // Always capture fatal errors
    if (context.severity === 'fatal') {
      return 1.0;
    }
    // Sample 10% of warnings
    if (context.severity === 'warning') {
      return 0.1;
    }
    // Sample 50% of regular errors
    return 0.5;
  },
});

Best Practices

1. Capture Early, Handle Gracefully

try {
  await riskyOperation();
} catch (error) {
  // Capture first
  obs.captureException(error as Error);
 
  // Then handle gracefully
  return fallbackValue;
}

2. Add Meaningful Context

obs.captureException(error, {
  tags: {
    feature: 'payment',
    provider: 'stripe',
  },
  extra: {
    orderId: order.id,
    amount: order.total,
    currency: order.currency,
  },
});

3. Don't Over-Capture

// Bad - captures expected validation errors
try {
  validateInput(data);
} catch (error) {
  obs.captureException(error); // Noise!
  showValidationError(error);
}
 
// Good - only capture unexpected errors
try {
  await processData(validatedData);
} catch (error) {
  obs.captureException(error);
  showGenericError();
}

4. Use Appropriate Severity

// Fatal - app can't continue
obs.captureMessage('Database connection lost', 'fatal');
 
// Error - operation failed
obs.captureException(paymentError, { severity: 'error' });
 
// Warning - degraded but functional
obs.captureMessage('Cache miss, using database', 'warning');

Next Steps