---
name: exception-triage
description: Use when the user mentions an exception, crash, EAccessViolation, EInvalidPointer, EConvertError, EOutOfMemory, "raise", "stack trace", or any other Delphi/Windows runtime failure they want to understand. Covers catch-all breakpoints, first-chance vs second-chance filtering, frame walking to user code, and exception-object inspection.
---

# Exception triage with rdbg

rdbg pushes `exception` events automatically as the target raises
exceptions. Each event carries `kind: "delphi" | "os"`,
`first_chance` (bool), `code`, `address`, `class?` (for Delphi),
and `frames[]`. Your job is to turn that into a one-line
diagnosis with a `.pas:line` reference.

## Set the catch-all up front

Before `continue`-ing the target after attach, set a catch-all
exception breakpoint. The simplest version is a symbol BP on
`RaiseException`:

```
set_breakpoint(kind="symbol", symbol="kernelbase!RaiseException")
```

This fires on **every** Delphi raise (RTL `raise` lowers to
`KernelBase.RaiseException`), before any user `try/except`
catches it. You see exceptions even when the application
silently swallows them.

For OS-level access violations and stack overflows, the
`exception` event fires unprompted from rdbg's debug-event
loop — no breakpoint needed.

## First-chance vs second-chance

- **`first_chance: true`** — the OS / RTL is offering the
  exception; user code has a chance to catch it. Most
  first-chance events are noise (handled exceptions are normal
  in VCL apps).
- **`first_chance: false`** — second-chance, no handler caught
  it, the process is about to terminate. This is what "the user
  saw a crash" means.

For "intermittent crash" reports, set
`set_exception_filter(first_chance=false)` so only unhandled
exceptions queue up. For "investigate why this raise happens at
all," leave the filter off and walk first-chance events.

To filter by class:

```
set_exception_filter(classes=["EAccessViolation", "EInvalidPointer"])
```

## Walking frames to user code

On an `exception` event, `frames[]` is a backtrace from innermost
to outermost. The first few frames are usually system DLLs:

```
[0] kernelbase!RaiseException + 0x6c
[1] System.SysUtils.RaiseLastOSError + 0x40
[2] System.SysUtils.IOResultCheck + 0x18
[3] MyApp.OpenFile + 0x4d at MyApp.pas:128   ← FIRST USER FRAME
[4] MyApp.Form1.Button1Click + 0x12 at MyApp.pas:45
```

The first user-code frame (the one with a `.pas` source
reference) is almost always where the bug lives. Cross-reference
with the source via `Read`/`Grep` to read the surrounding code.

## Inspecting the exception object

Delphi exceptions are `TObject` instances. For first-chance
Delphi exceptions the daemon already decodes the exception object
and emits its address as `exception_object` on the `exception`
event — read it directly. For OS exceptions the address is at
`frames[0].address`.

If you ever need to read the `RaiseException` arguments by
register, branch on the response's `arch` field: on x64 the
stdcall mapping is `rcx`=code, `rdx`=flags, `r8`=nargs,
`r9`=args pointer; on x86 the args are stack-relative (push
order), so `read(kind="memory", address=esp+4)` for code,
`+8` flags, `+12` nargs, `+16` args pointer.

Once you have the address:

```
inspect_object(address=<excaddr>, max_depth=2)
```

Returns the exception class (`EAccessViolation`, etc.) and the
`Message` field (a Delphi string — `read(kind="string", address)`
to materialize).

## Common patterns

- **"Why does it crash sometimes?"** → second-chance only,
  collect crash sequences, look for input commonalities.
- **"Why is this exception swallowed?"** → all first-chance,
  walk frames, find the `try/except` that catches it; the user
  may want a log line or a re-raise.
- **"Stack overflow"** → `EStackOverflow` is special; frames
  may be truncated. Look at the deep recursion in the symbol
  pattern (one method appearing many times).
- **"Access violation reading 0x0..."** → null deref. Walk back
  to the user frame, see what was supposed to be assigned.

## Reporting

Always report exceptions as:

> `EAccessViolation` at `MyApp.pas:128` in `OpenFile`. Message:
> "Access violation at address 00007FF6... read of address
> 0000000000000000". Cause: `FStream` is `nil` when `OpenFile`
> is called from `Button1Click` because `LoadConfig` (line 92)
> only assigns `FStream` on the success branch.

That gives the user the file:line, the class, the message, and
the *why*.
