Quick answer: D games crash through the Throwable hierarchy, which splits into recoverable Exception and fatal Error, and through native faults below druntime. Catch Throwable at your top level to capture stack traces druntime can produce, but know Errors signal unrecoverable conditions and segfaults bypass the hierarchy entirely. Add signal handlers, mind the GC and nogc paths, and report with build context so each crash is readable.

D is a pragmatic systems language with a real type system, a garbage collector you can opt out of, and a following among indie developers who want C++ power with less ceremony. Its crash model has its own shape: a Throwable hierarchy that distinguishes recoverable exceptions from fatal errors, a runtime called druntime that can produce stack traces, and a garbage collector whose presence or absence changes the picture. Underneath it all is a native binary that faults like any other. This post covers catching the Throwable hierarchy, working with druntime, handling native signals, and the GC considerations that shape crash reporting in D.

The Throwable hierarchy

D's error handling centers on the Throwable base class, which splits into two branches with very different meanings. Exception represents recoverable conditions you are expected to catch and handle, a missing file, a parse failure. Error represents unrecoverable conditions, an assertion failure, an out of bounds array access, an out of memory, that signal a broken program state. The distinction is deliberate: catching Exception is normal control flow, while catching Error is discouraged because after an Error the program may be in an inconsistent state you cannot safely continue from.

For crash reporting this means your top level handler should catch Throwable, the common base, so it sees both branches, but it should treat them differently. An uncaught Exception that reached the top is a bug you want reported and then a clean exit. An Error is a genuine crash condition, and catching it at the top is acceptable precisely to capture a report before terminating, not to recover and continue. druntime attaches a stack trace to thrown Throwables, so the handler can read a real trace from the object, which is the most reliable stack you will get for the failures that travel through the hierarchy.

druntime and stack traces

druntime is D's runtime library, and it does a lot of the work that makes D crashes readable. It installs default handlers, manages the garbage collector, and crucially attaches a backtrace to thrown Throwables, so when you catch one you can print or capture a stack with function names and lines, provided the build kept debug information. This is a meaningful advantage over a bare native language: for the large class of crashes that come through assertions, bounds checks, and thrown errors, you get a usable stack without writing the unwinding yourself.

druntime also installs its own signal handling on some platforms to convert certain hardware faults into more descriptive output, but you should not assume it captures everything you need for field reporting. The default behavior is oriented toward printing a trace and aborting, not toward uploading a structured report with context. So you build on top of druntime: let it give you the Throwable stack traces, but add your own top level Throwable handler that captures a structured report, and add native signal handling for the faults that do not enter the Throwable path at all.

Native faults and the GC

Below the Throwable hierarchy, D is still a native language, and a genuine segfault from a dangling pointer in nogc code, a buffer overrun, or a stack overflow can arrive as a raw signal that druntime's Throwable machinery does not turn into a catchable Error. For those you install native signal handlers, the same SIGSEGV and friends on an alternate stack approach as any native game, and capture a backtrace to symbolicate. D games that opt into nogc for performance, or use betterC to strip the runtime, give up some of druntime's help and lean more heavily on this native layer.

The garbage collector also shapes the crash picture in subtler ways. A GC pause is not a crash but can look like a hang under tight frame budgets, and out of memory conditions surface as Errors. Code that mixes GC managed and manually managed memory, or that calls into C libraries, opens the door to native faults from lifetime mistakes. Knowing whether a given subsystem runs under the GC or in nogc territory helps you interpret a crash, so recording the relevant build configuration and which paths are nogc as context makes native faults in a D game quicker to localize.

Building a complete report

A thorough D crash report draws from both layers. From a caught Throwable you get the type, whether it was an Exception or an Error, the message, and druntime's attached stack trace with names and lines. From a native signal handler you get the faulting context and a backtrace to symbolicate against the build's debug info. Tagging which layer produced the report, and for Throwables which branch of the hierarchy, immediately tells you the nature of the failure: a logic error D expressed, a fatal Error condition, or a raw memory fault that escaped the runtime entirely.

Round it out with the standard context: build version, platform, compiler used since DMD and LDC can differ in behavior, and your game state. Keep debug symbols per build so both the druntime traces in release and the native backtraces resolve to source. The combination gives you reports that are readable across D's whole range of failure modes, from a tidy assertion failure with a named stack to a segfault in nogc interop code, each arriving with enough context to act on rather than as an undifferentiated crash.

Setting it up with Bugnet

Bugnet stores D crashes from both the Throwable path and the native signal path in one dashboard with context attached, so an uncaught Error with a druntime stack trace and a raw segfault with a symbolicated backtrace sit together and comparable. From your top level Throwable handler and your signal handler you send the stack, the message, the layer and hierarchy tag, and custom fields like the compiler and whether the failing path was nogc, turning each D crash into a contextual issue. The in game report button covers the non crashing problems players want to describe with their state.

Like any native game, a D game sees the same crash recur across many players, and occurrence grouping folds identical stacks into one issue with a count so you triage once and prioritize by impact. Filtering by platform or compiler tells you whether a crash is universal or tied to one toolchain, which matters in D where DMD and LDC builds can behave differently. For a language with a smaller ecosystem than the big engines, having a general destination that accepts your reports with the context you choose gives a D game the same field visibility a mainstream stack enjoys.

Verifying the layers

The trap to avoid is a setup that catches Throwables and feels complete while missing the native faults that bypass druntime, which in nogc or betterC heavy code may be a large share of real crashes. Test deliberately: throw an uncaught Exception and an Error and confirm the Throwable path reports each with a druntime stack, then force a segfault in nogc code and confirm the native signal handler reports it symbolicated. Do this per platform and per compiler, since druntime's behavior and symbol formats vary across DMD, LDC, and operating systems.

With both layers verified and symbols archived per build, a D game has crash visibility as complete as any mainstream engine. You see your exceptions, your fatal errors, and your native faults, grouped and contextual, and you fix the highest impact ones first. D gives you a helpful runtime in druntime and the option to step outside it for performance; a crash pipeline that covers both keeps that flexibility from costing you observability, so the game stays debuggable in the field whatever balance of GC and nogc you chose.

D splits failures into Exception, Error, and native faults. Catch Throwable for the first two, add signal handlers for the third, and tag which is which.