Windows Vectored Exception Handlers: Observation, Ordering, and Safe Teardown
Design narrow Windows vectored exception observers with deliberate ordering, allocation-free callbacks, registration ownership, and safe DLL teardown.
A vectored exception handler is registered for the process rather than attached to a particular stack frame. Windows can invoke it when an exception occurs anywhere in that process. This makes it useful for narrowly scoped instrumentation or runtime mechanisms that intentionally understand a particular exception, but it also gives a faulty handler a large failure surface.
The safest starting point is observation: inspect only the exception information required by the feature and continue the normal handler search. Registering a callback does not make arbitrary crashes recoverable, prove that the process remains consistent, or replace a dump-based diagnosis workflow. Native desktop support begins with Windows XP for the registration API; build and test against the Windows SDK and architectures your product actually supports.
Know where the callback sits
Microsoft documents vectored exception handling as an extension to structured exception handling. A debugger receives its first-chance notification before vectored handlers, and vectored handlers run before stack unwinding starts. They are not bound to the frame where the registration happened.
That timing matters when investigating “crashes” reported by an observer. A first-chance exception may subsequently be handled by an ordinary application mechanism. Seeing an exception is not proof that it became unhandled. Record the exact event your instrumentation observed and correlate it with debugger behavior, process termination, or a crash dump rather than counting every invocation as a fatal failure.
An exception handler and a vectored continue handler are separate registrations. The continue-handler API has its own registration and removal functions. Do not mix their tokens or assume that registering one automatically registers the other.
Own registration as a resource
AddVectoredExceptionHandler returns an opaque token on success and null on failure. Pass that token to RemoveVectoredExceptionHandler when the owner no longer needs the callback. The token is not an operating-system handle to be passed to CloseHandle.
The First argument chooses whether to insert the handler at the beginning or end of the current ordering. A nonzero value does not establish permanent precedence: a later registration can install another first handler. Shared libraries therefore should not build correctness around being globally first unless the entire process explicitly controls that contract.
This observer example filters one application-defined exception code and counts invocations using an interlocked operation. It does not allocate, format strings, perform I/O, or attempt recovery inside the callback:
#include <windows.h>
constexpr DWORD kObservedCode = 0xE0421001;
alignas(4) volatile LONG observedCount = 0;
LONG CALLBACK ObserveException(EXCEPTION_POINTERS* information) {
if (information != nullptr &&
information->ExceptionRecord != nullptr &&
information->ExceptionRecord->ExceptionCode == kObservedCode) {
InterlockedIncrement(&observedCount);
}
return EXCEPTION_CONTINUE_SEARCH;
}
PVOID InstallObserver() {
return AddVectoredExceptionHandler(0, ObserveException);
}
bool UninstallObserver(PVOID registration) {
return registration != nullptr &&
RemoveVectoredExceptionHandler(registration) != 0;
}
The application must check installation failure and retain the returned token in a well-defined owner. The global counter is illustrative bounded telemetry, not a comprehensive crash record. It can overflow in an extreme long-running workload; a production design should define its counting/saturation policy. Reset or read it with the same interlocked synchronization discipline while callbacks can run.
This snippet does not trigger exceptions by itself. A dedicated Windows test executable can raise the selected code inside a structured exception test region and verify that the observer increments while ordinary handling still runs. Keep that experiment out of production request paths.
Keep the callback small enough to survive failure
Microsoft warns that vectored callbacks should not call functions that allocate memory or acquire synchronization objects. The exception may have interrupted code while it held a lock or while allocator state was already damaged. A normal logging library can do both, even when its call site looks like a harmless single function.
Formatting stack traces, loading symbols, opening files, taking a global logger lock, or allocating a JSON record inside the callback can introduce another failure or a deadlock. If the product needs richer reporting, establish a separate diagnostic mechanism and decide which minimal facts can be transferred safely. Do not keep pointers to the exception record or context for later dereference; their validity is tied to exception dispatch, so deferred analysis needs an intentionally captured representation.
The callback can run on different faulting threads, so mutable shared instrumentation must tolerate concurrency. It should also tolerate exceptions unrelated to the feature. Narrow filtering is easier to audit than interpreting every exception code in the process.
Continuing execution requires a specific repair contract
The callback return value distinguishes continuing the handler search from continuing execution at the exception context. An observer returns EXCEPTION_CONTINUE_SEARCH. Returning EXCEPTION_CONTINUE_EXECUTION asserts that execution can safely resume, which is a much stronger claim.
A generic access violation, illegal instruction, or corrupted stack does not meet that contract merely because a handler saw it. Repeating the same instruction can immediately fault again. Skipping instructions by changing the instruction pointer is architecture-specific and can bypass required effects. Microsoft’s historical sample explicitly warns that it is not suitable for 64-bit Windows; do not copy its context edits into a cross-architecture production handler.
Runtime systems that deliberately recover particular exceptions need a narrowly defined ownership test, valid machine-context repair, and detailed architecture-specific verification. They must still let all unrelated exceptions proceed through normal handling. Exception swallowing as a general “stability” feature hides defects and can preserve corrupted application state.
Teardown must protect callback code and state
Windows does not automatically unregister a handler when the DLL containing its callback is unloaded. A remaining registration can later direct exception dispatch into unmapped code. Remove the registration before unloading, and retain both the code module and callback state for every invocation that may still be active.
The removal API documents registration removal; do not infer a general join/barrier for application activity from it. Design teardown around controlled worker shutdown and known callback lifetimes. A plugin architecture that cannot establish those conditions should keep its observer in a stable host module or retain the module until process exit.
Avoid relying on a destructor running during an uncontrolled crash to clean up registration. Normal startup/shutdown ownership must be explicit. If removal fails, record that failure outside exception dispatch and keep any required code/state alive until the process terminates or a supported retry resolves the registration.
Acceptance and diagnosis
Verify successful and failed registration, ordinary handled exceptions, unrelated exception codes, simultaneous exceptions on several threads, ordered registrations, and controlled removal. Run with and without a debugger because first-chance stops affect observed execution. Check x64 and ARM64 separately if both are supported, especially if any code examines processor context.
For a real unhandled crash, preserve WER dumps or an equivalent supported capture workflow and reproduce under a debugger with Application Verifier or Page Heap when appropriate. The observer’s count answers only how often its filtered callback ran. It does not identify the root cause or establish that the machine state was safe after an exception.
Vectored handlers are a process-wide mechanism with strict lifetime and callback constraints. Keeping them narrow, observable, and removable makes their behavior reviewable without turning exception dispatch into another source of failures.
Related:
- Windows WER LocalDumps: Capturing User-Mode Crashes for Diagnosis
- Application Verifier and Page Heap: Catch Native Windows Bugs at the Fault Site
Sources: