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)).
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.
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
1. What is the fundamental distinction between D's Exception and Error hierarchies?
2. When does a scope(failure) block execute?
3. What does std.exception.enforce(condition, message) do?
4. What guarantee does marking a function nothrow provide, and how is it enforced?
5. What happens if assumeWontThrow wraps an expression that actually throws at runtime?
Was this page helpful?
You May Also Like
Concurrency in D
Explore D's message-passing actors in std.concurrency, shared-memory safety via shared/synchronized/immutable, and declarative data parallelism with std.parallelism.
Compile-Time Function Execution (CTFE)
Understand how D executes ordinary functions at compile time to compute constants, validate data, and generate code before the program ever runs.
Ranges in D
Learn how D's range interface unifies iteration over arrays, containers, and lazy sequences, and how to compose algorithms with std.range and std.algorithm.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics