# Capture exceptions for error tracking - Docs

You can track and monitor errors and exceptions in your code by capturing [exception events](/docs/error-tracking/issues-and-exceptions.md). This can be done automatically when exceptions are thrown in your code, or manually by calling the exception capture method.

## Capturing exceptions

Exceptions are a special type of [event](/docs/data/events.md) in PostHog. Similar to any other event, they can be captured, customized, filtered, and used in insights for analysis.

To help group exceptions into issues and help you debug them, PostHog automatically captures the following properties:

| Name | Key | Example value |
| --- | --- | --- |
| $exception_list | List | A list of exceptions that occurred. In languages that support chained exceptions, the list will contain multiple items. Contains the following properties: |
| └─ type | String | The type of exception that occurred |
| └─ value | String | The message of the exception that occurred |
| └─ stacktrace | Object | Contains a list of stack frames that occurred |
| └─ mechanism | Object | If the stacktrace is handled and if it's synthetic |
| $exception_fingerprint | String | A fingerprint of the exception |
| $exception_level | String | The level of the severity of the error |

Like normal events, it's important to [identify the user](/docs/product-analytics/identify.md) when capturing exceptions.

**Source maps**

If you serve minified or compiled code, PostHog needs source maps to display the correct stack traces. [Configure source maps](/docs/error-tracking/upload-source-maps.md) to get the most out of your exception events.

### Automatic exception capture

If you followed one of our guides to [set up error tracking](/docs/error-tracking/installation.md) and you [enabled exception auto capture](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-exception-autocapture), you'll have automatic exception capture enabled.

For browser SDKs (JavaScript Web, React, Next.js, etc.), you can configure which types of errors are automatically captured using the `capture_exceptions` option:

JavaScript

PostHog AI

```javascript
posthog.init('<ph_project_token>', {
    api_host: '<ph_api_client_host>',
    defaults: '2026-01-30',
    capture_exceptions: {
      capture_unhandled_errors: true, // default
      capture_unhandled_rejections: true, // default
      capture_console_errors: false // default
  }
})
```

| Option | Default | Description |
| --- | --- | --- |
| capture_unhandled_errors | true | Captures uncaught errors via window.onerror. |
| capture_unhandled_rejections | true | Captures unhandled promise rejections via window.onunhandledrejection. |
| capture_console_errors | false | Captures calls to console.error as exception events. |

You can also pass `capture_exceptions: true` to enable all default options, or `capture_exceptions: false` to disable autocapture entirely.

### Manual exception capture

You can also manually call the exception capture method.

**Always use captureException()**

Never manually capture `$exception` events using `posthog.capture('$exception', {...})`. Always use `posthog.captureException(error)` instead – it handles the correct event format, stack trace processing, and source map integration automatically.

PostHog AI

### Web

```javascript
posthog.captureException(error, {
  custom_property: "custom_value",
  custom_list: ["custom_value_1", "custom_value_2"],
})
```

### Node.js

```javascript
posthog.captureException(e, 'user_distinct_id', {
  custom_property: "custom_value",
  custom_list: ["custom_value_1", "custom_value_2"],
})
```

### Python

```python
additional_properties = {
    "custom_property": "custom_value",
    "custom_list": ["custom_value_1", "custom_value_2"],
}
posthog.capture_exception(
    e,
    distinct_id="user_distinct_id",
    properties=additional_properties
)
```

### Ruby

```ruby
posthog.capture_exception(
  e,
  'user_distinct_id',
  {
    custom_property: 'custom_value',
    custom_list: ['custom_value_1', 'custom_value_2']
  }
)
```

### Go

```go
exception := posthog.NewDefaultException(
    time.Now(),
    "user_distinct_id",
    "ErrorType",
    err.Error(),
)
client.Enqueue(exception)
```

### .NET

```csharp
posthog.CaptureException(
    exception,
    "user_distinct_id",
    new Dictionary<string, object>
    {
        ["custom_property"] = "custom_value",
        ["custom_list"] = new[] { "custom_value_1", "custom_value_2" },
    }
);
```

### Unity

```csharp
PostHog.CaptureException(
    exception,
    new Dictionary<string, object>
    {
        { "custom_property", "custom_value" },
        { "custom_list", new[] { "custom_value_1", "custom_value_2" } },
    }
);
```

## Add exception steps

> Available in `posthog-js` 1.370.0 and later.

Exception steps are lightweight breadcrumbs you attach to an exception before calling `captureException(...)`. They are buffered in memory, added to the next exception as `$exception_steps`, and shown in the session timeline.

Use them for high-signal checkpoints like:

-   Checkout or onboarding milestones
-   API calls and retries
-   Feature flag decisions
-   Validation checkpoints

### How they work

-   Steps are buffered locally until an exception is captured.
-   They are attached to the exception under `$exception_steps`.
-   They are not sent as standalone PostHog events.
-   Buffered steps are also included on automatically captured exceptions.

### Example

JavaScript

PostHog AI

```javascript
posthog.addExceptionStep('checkout_started', {
  cart_total: 12900,
  item_count: 3,
})
posthog.addExceptionStep('payment_attempted', {
  provider: 'stripe',
})
try {
  await confirmCardPayment(clientSecret)
} catch (error) {
  posthog.captureException(error, {
    checkout_id: 'chk_123',
  })
}
```

### Configure limits

The buffer is byte-aware: when the budget is exceeded, the oldest steps are evicted. The default is 32KB.

JavaScript

PostHog AI

```javascript
posthog.init('<ph_project_token>', {
  api_host: '<ph_api_client_host>',
  defaults: '2026-01-30',
  error_tracking: {
    exception_steps: {
      enabled: true,
      max_bytes: 32768,
    },
  },
})
```

### Best practices

-   Use short step names that describe a checkpoint or action.
-   Keep payloads small and focused.
-   Avoid sensitive data, raw request bodies, and large nested objects.
-   Use the `before_send` callback to redact exception steps before they are sent.
-   Add steps before the risky operation runs.

### Troubleshooting missing steps

If an exception appears in error tracking but not in the timeline with steps:

-   Make sure you called `addExceptionStep(...)` before the failure.
-   Use `captureException(...)` or autocapture, not `posthog.capture('$exception', ...)`.
-   Reduce step payload size if the final event is too large.

### Error boundaries

Error boundaries let you catch rendering errors in your component tree and report them to PostHog. We currently support error boundaries for:

-   **React** – Use the `PostHogErrorBoundary` component. See the [React error boundaries guide](/docs/error-tracking/installation/react.md#set-up-error-boundaries).
-   **React Native** – Use the `PostHogErrorBoundary` component. See the [React Native error boundaries guide](/docs/error-tracking/installation/react-native.md#set-up-error-boundaries).

## Customizing exception capture

Like all data, the better **quality** of your exception events, the more **context** you'll have for debugging and analysis. Customizing exception capture helps you do this.

Customizing exception capture lets you override exception properties to influence how they're grouped into issues.

Equally important, you can customize properties on the exceptions to help you configure rules for [automatic issue assignment](/docs/error-tracking/assigning-issues.md#automatic-issue-assignment), [alerts](/docs/error-tracking/alerts.md), [issue grouping](/docs/error-tracking/grouping-issues.md). They can also be used in analysis in [insights](/docs/product-analytics/insights.md), [dashboards](/docs/product-analytics/dashboards.md), and [data warehouse queries](/docs/data-warehouse/start-here.md).

### Customizing exception properties

#### During manual capture

When capturing exceptions manually, passing properties to the capture exception method adds them to the event just like any other PostHog event.

You can also override the [fingerprint](/docs/error-tracking/fingerprints.md) to [group](/docs/error-tracking/issues-and-exceptions.md#how-are-issues-grouped) exceptions together at capture time.

Here are some examples of how to override exception properties:

PostHog AI

### Web

```javascript
try {
  // ...
} catch (error) {
 posthog.captureException(error, {
      "custom_property": "custom_value",
      // Optional: you can also override generated properties like fingerprint
      "$exception_fingerprint": "CustomExceptionGroup",
    });
}
```

### Node.js

```javascript
try {
  // ...
} catch (error) {
 posthog.captureException(error, 'user_123', {
      "custom_property": "custom_value",
      // Optional: you can also override generated properties like fingerprint
      "$exception_fingerprint": "CustomExceptionGroup",
    });
}
```

### Python

```python
import posthog
posthog.api_key = '<ph_project_token>'
posthog.host = 'https://us.i.posthog.com'
# With context
with posthog.new_context():
    posthog.identify_context(distinct_id="user_123")
    # When using context, you define properties with tags
    posthog.tag("custom_property", "custom_value")
    # Optional: you can also override generated properties like fingerprint
    posthog.tag("$exception_fingerprint", ["custom_fingerprint"])
    posthog.capture_exception(Exception("Test custom exception"))
# Or without context
posthog.capture_exception(
    Exception("Test custom exception without context"),
    distinct_id="user_123",
    properties={
        "$exception_type": "Custom exception without context",
        "$exception_fingerprint": ["custom_fingerprint without context"],
        "custom_property": "custom_value",
    },
)
```

### Ruby

```ruby
begin
  subscriptions.activate(user_id, subscription_id)
rescue => e
  posthog.capture_exception(
    e,
    'user_123',
    {
      subscription_id: subscription_id,
      # You can also *optionally* override generated properties like fingerprint
      '$exception_fingerprint': 'CustomExceptionGroup'
    }
  )
end
```

### Go

```go
fingerprint := "CustomExceptionGroup"
exception := posthog.Exception{
    DistinctId: "user_123",
    Timestamp:  time.Now(),
    ExceptionList: []posthog.ExceptionItem{
        {
            Type:  "ActivationError",
            Value: err.Error(),
        },
    },
    // Optional: override the fingerprint for custom grouping
    ExceptionFingerprint: &fingerprint,
}
client.Enqueue(exception)
```

#### During automatic capture

When automatic exception capture is enabled, you can still override the default properties and add custom properties to the exception event.

You can also override the [fingerprint](/docs/error-tracking/fingerprints.md) to [group](/docs/error-tracking/issues-and-exceptions.md#how-are-issues-grouped) exceptions together at capture time.

The process is slightly different depending on the SDK you're using.

## Web and Node.js

In [JavaScript Web](/docs/libraries/js.md) and [Node.js](/docs/libraries/node.md) SDKs, you can override the default properties by passing a `before_send` callback. This callback is called before any exception is captured.

JavaScript

PostHog AI

```javascript
posthog.init('<ph_project_token>', {
api_host:'https://us.i.posthog.com',
defaults: '2026-01-30',
before_send: (event) => {
  if (event && event.event === "$exception") {
    const exceptionList = event.properties?.["$exception_list"] || []
    const exception = exceptionList.length > 0 ? exceptionList[0] : null;
    if (exception) {
      event.properties["custom_property"] = "custom_value"
      // Optional: you can also override generated properties like fingerprint
      event.properties["$exception_fingerprint"] = "MyCustomGroup"
      // ... and any other properties you want to override
    }
  }
  return event
  }
})
```

## React Native

In [React Native](/docs/libraries/react-native.md), you can override the default properties by passing a `before_send` callback when initializing PostHog. This callback is called before any exception is captured.

React Native

PostHog AI

```jsx
const posthog = new PostHog('<ph_project_token>', {
  host: 'https://us.i.posthog.com',
  before_send: (event) => {
    if (event.event === '$exception') {
      const exceptionList = event.properties?.['$exception_list'] || []
      const exception = exceptionList.length > 0 ? exceptionList[0] : null
      if (exception) {
        event.properties['custom_property'] = 'custom_value'
        // Optional: you can also override generated properties like fingerprint
        event.properties['$exception_fingerprint'] = 'MyCustomGroup'
        // ... and any other properties you want to override
      }
    }
    return event
  },
})
```

## Python

In Python, you can override the default properties through the use of [contexts](/docs/libraries/python.md#contexts) and tags.

Python

PostHog AI

```python
import posthog
posthog.api_key = '<ph_project_token>'
posthog.host = 'https://us.i.posthog.com'
# With context
with posthog.new_context():
    posthog.identify_context(distinct_id="<user_distinct_id>")
    posthog.set_context_session(session_id="<session_id>")
    posthog.tag("custom_property", "custom_value")
    # Optional: you can also override generated properties like fingerprint
    posthog.tag("$exception_fingerprint", ["custom_fingerprint"])
    # When this exception is automatically captured, it will be tagged with the custom properties
    raise Exception("Test custom exception")
```

### Capturing properties for custom issue grouping rules

Other than grouping with [custom fingerprints](#customizing-exception-properties), you can also set custom properties on the exception to help you group exceptions together using [issue grouping rules](/docs/error-tracking/grouping-issues.md).

For example:

-   Setting fields like `db_transaction` to group exceptions together for a specific database transactions.
-   Setting `feature_name`, `service_name`, or `service_version` to group exceptions together for a specific features and services.
-   Setting `intent` to group exceptions due to common interactions like accessing storage or network.

It's important to think about your grouping rules when you configure exception capture. You cannot group exceptions together if you don't set some common properties between them.

Note that grouping issues means that all exceptions will be grouped together under a **single issue**. You **cannot** group exceptions into multiple issues, but you can still [filter on custom properties](/docs/error-tracking/monitoring.md#finding-specific-issues) across multiple issues without grouping them.

## Suppressing exceptions

We recommend that you suppress exceptions on the client side for performance and cost reasons.

You can use the `before_send` callback in the [web](/docs/libraries/js.md), [Node.js](/docs/libraries/node.md), and [React Native](/docs/libraries/react-native.md) SDKs to exclude any exception events you do not wish to capture. Do this by providing a `before_send` function when initializing PostHog and have it return a falsey value for any events you want to drop.

PostHog AI

### Web

```javascript
posthog.init('<ph_project_token>', {
    before_send: (event) => {
        if (event.event === "$exception") {
            const exceptionList = event.properties["$exception_list"] || []
            const exception = exceptionList.length > 0 ? exceptionList[0] : null;
            if (exception && exception["$exception_type"] === "UnwantedError") {
                return false
            }
        }
        return event
    }
})
```

### React

```jsx
const posthog = new PostHog('<ph_project_token>', {
    host: 'https://us.i.posthog.com',
    before_send: (event) => {
        if (event.event === '$exception') {
            const exceptionList = event.properties?.['$exception_list'] || []
            const exception = exceptionList.length > 0 ? exceptionList[0] : null
            if (exception && exception['$exception_type'] === 'UnwantedError') {
                return null
            }
        }
        return event
    },
})
```

You can also suppress exceptions in PostHog, these rules will be applied client-side.

[Suppression rules](https://app.posthog.com/error_tracking/configuration#selectedSetting=error-tracking-suppression-rules) drop matching future exceptions before they are ingested as `$exception` events, so they can reduce your bill. Supported SDKs may apply rules client-side before sending exceptions to PostHog, and PostHog also applies them server-side before storing the event. They do not apply retroactively to exceptions already ingested. You can only filter based on the exception type and message attributes because the stack of an exception may still be minified client-side.

![Issue suppression rules](https://res.cloudinary.com/dmukukwp6/image/upload/suppression_rule_light_8db44eb6b1.png)![Issue suppression rules](https://res.cloudinary.com/dmukukwp6/image/upload/suppression_rule_dark_5972d57398.png)

## Burst protection

The JavaScript web SDK uses burst protection to limit the number of autocaptured exceptions that can be captured in a period. This prevents an excessive amount of exceptions being captured from any one client, typically because they're being thrown in an infinite loop.

By default, we capture 10 exceptions (bucket size) of the same type within a 10 second period before the rate limiter kicks in, after which, we capture 1 exception (refill rate) every 10 seconds.

Often not needed, but you can change the bucket size and refill rate as part of your configuration:

JavaScript

PostHog AI

```javascript
import posthog from 'posthog-js'
posthog.init('<ph_project_token>', {
    api_host: 'https://us.i.posthog.com',
    error_tracking: {
        __exceptionRateLimiterRefillRate: 1
        __exceptionRateLimiterBucketSize: 10
    }
})
```

## Common problems

### Exception steps don't appear in the session timeline

If you can see the exception in error tracking but not its steps in the timeline, the most common causes are:

-   The steps were recorded after the exception was captured.
-   The server-side code didn't have an active request or job scope.
-   The exception wasn't linked to the same user or session as the replay.
-   The step payload was too large and got truncated or dropped.

Start by verifying identification and session linkage, then confirm your steps are being added immediately before the risky operation.

### Don't use `posthog.capture()` for exceptions

**Don't do this**

**Do not** use `posthog.capture('$exception', { ... })` and try to attach exception metadata yourself (e.g. `$exception_stack_trace_raw`, `$exception_type`, `$exception_message`). This approach fails in the vast majority of cases because the exception event schema is strict and the SDK does not normalize or validate these properties when you use `capture()`.

**Do this instead:** use `posthog.captureException(error)`. It accepts an `Error` object (or your runtime's equivalent), extracts stack trace, type, and message correctly, and sends a valid `$exception` event with all required metadata. You can still pass optional properties as the second argument.

### Community questions

Ask a question

### Was this page useful?

HelpfulCould be better