---
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. Filtered
exceptions are dropped at both the event-emit AND pause sites —
your filter actually frees the agent from drowning in noise; the
target keeps running through filtered raises.

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. **Each frame carries a `kind` field**: `user`,
`system`, `rtl`, or `unknown`:

```
[0] {kind: "system", module_name: "kernelbase.dll", symbol: "RaiseException", offset: 0x6c}
[1] {kind: "rtl",    module_name: "rtl290.bpl",     symbol: "System.SysUtils.RaiseLastOSError", offset: 0x40}
[2] {kind: "rtl",    module_name: "rtl290.bpl",     symbol: "System.SysUtils.IOResultCheck", offset: 0x18}
[3] {kind: "user",   module_name: "MyApp.exe",      symbol: "MyApp.OpenFile",  source: "MyApp.pas", line: 128}  ← FIRST USER FRAME
[4] {kind: "user",   module_name: "MyApp.exe",      symbol: "MyApp.Form1.Button1Click", source: "MyApp.pas", line: 45}
```

**Filter `frames[]` for `kind == "user"` to get the user-code
backtrace directly.** The first `user` frame is almost always
where the bug lives. Cross-reference with the source via
`Read`/`Grep` to read the surrounding code.

If `frames[]` ends with a `kind: "unknown"` frame and there are no
`kind: "user"` frames after, the stack walker truncated at an
unwind boundary it couldn't bridge. The exception's `address`
field still tells you the raise site; pair it with the target's
.map file (or `lsp_definition` if you have a project loaded) to
narrow down the user code that called into the unwound frame.

## 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*.
