100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Error Handling in D

Learn D's split between recoverable Exceptions and unrecoverable Errors, the scope(exit)/failure/success guards, and std.exception helpers like enforce and nothrow.

Advanced DIntermediate9 min readJul 10, 2026
Analogies

Exception vs Error: D's Two Hierarchies

D deliberately splits its throwable hierarchy into two roots that mean very different things: Exception (and its subclasses) represents recoverable conditions that a well-written program is expected to catch and handle — a missing file, invalid user input — while Error (and subclasses like AssertError, RangeError, and OutOfMemoryError) represents unrecoverable programmer or runtime-invariant violations that indicate a bug, and idiomatic D code should generally not catch Error at all, letting the program terminate rather than continuing in a possibly corrupted state. This distinction lets a try/catch(Exception e) block safely assume it will never accidentally swallow a failed assert or an out-of-bounds array access, since those derive from Error, not Exception.

🏏

Cricket analogy: It's like the difference between a rain delay (Exception) — a recoverable interruption the match officials plan around and resume from — versus the pitch itself literally collapsing mid-over (Error), which abandons the match entirely because play genuinely cannot safely continue.

scope(exit), scope(failure), scope(success)

D provides three scope guard statements as a lightweight, built-in alternative to manually paired try/finally blocks: scope(exit) { ... } runs its body when the enclosing scope is left for any reason (normal return or exception), scope(failure) { ... } runs only if the scope is left via an exception (useful for rollback logic), and scope(success) { ... } runs only if the scope completes normally. Multiple scope statements in the same block execute in reverse (LIFO) order relative to their declaration, mirroring how you'd naturally want cleanup to unwind — the last resource acquired is the first one released — which makes them a common, idiomatic replacement for RAII destructors when working with non-class resources like file handles or manually managed buffers.

🏏

Cricket analogy: It's like a groundskeeper who always covers the pitch when play ends regardless of result (scope(exit)), but only calls in the heavy roller for emergency repair specifically when a washout has damaged the surface (scope(failure)), and only opens the podium stage specifically when the match actually finishes with a result (scope(success)).

d
import std.stdio : File, writeln;

void processFile(string path)
{
    auto f = File(path, "r");
    scope(exit) f.close();          // always runs
    scope(failure) writeln("processFile failed, rolling back");
    scope(success) writeln("processFile completed cleanly");

    foreach (line; f.byLine())
    {
        if (line.length == 0)
            throw new Exception("empty line encountered");
        // ... process line ...
    }
}

scope(exit) is frequently preferred over a try/finally block in D because it keeps the cleanup code adjacent to the resource-acquisition line rather than at the bottom of a growing function, reducing the chance that a later refactor forgets to update the matching finally block.

std.exception Helpers and nothrow

std.exception provides enforce(condition, message) as a concise alternative to if (!condition) throw new Exception(message);, commonly chained with UFCS like value.enforce!CustomException("bad value"), plus collectException(expr) to capture a thrown exception into a variable for testing without a full try/catch, and assumeWontThrow(expr) to tell the compiler a call that's technically throwing is known safe in this context (used sparingly, since it's a promise the compiler can't verify). Functions can also be marked nothrow to declare at the type level that they will never let an Exception propagate out — the compiler enforces this by requiring every throwing call inside a nothrow function be wrapped in try/catch or itself be nothrow, which is important for interfacing with C callbacks or @nogc real-time code paths where unwinding an exception mid-callback would be unsafe or impossible.

🏏

Cricket analogy: It's like an umpire using a quick, standardized hand-signal check ('is the batter's bat grounded? no? out') instead of writing a lengthy report each time (enforce), while a stadium's emergency floodlight backup system must be certified to never fail mid-over under any circumstance (nothrow), since a light failure during a live delivery isn't something the rules allow for.

d
import std.exception : enforce;

struct BankAccount
{
    private double balance;

    void withdraw(double amount)
    {
        enforce(amount > 0, "withdrawal amount must be positive");
        enforce(amount <= balance, "insufficient funds");
        balance -= amount;
    }
}

assumeWontThrow silences the compiler's nothrow checking but does not add any actual safety net — if the wrapped expression does throw at runtime anyway, the program terminates via an unrecoverable error (druntime calls a fatal handler) instead of a catchable exception, so only use it when you have airtight proof the call cannot throw.

  • Exception represents recoverable conditions meant to be caught; Error represents unrecoverable bugs/invariant violations that should generally not be caught.
  • AssertError, RangeError, and OutOfMemoryError all derive from Error, not Exception, so a catch(Exception e) block won't accidentally swallow them.
  • scope(exit) runs on any exit path, scope(failure) only on exception unwinding, and scope(success) only on normal completion.
  • Multiple scope guards in one block run in LIFO order, mirroring safe resource-unwinding order.
  • std.exception.enforce(condition, message) is a concise, chainable alternative to manual if-throw boilerplate.
  • nothrow functions are compiler-checked to guarantee no Exception escapes, required for certain C-interop and real-time contexts.
  • assumeWontThrow is an unchecked promise to the compiler; a broken promise terminates the program fatally rather than throwing catchably.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#DProgrammingStudyNotes#ErrorHandlingInD#Error#Handling#Exception#Two#ErrorHandling#StudyNotes#SkillVeris