The solution to "Error handling"...

ABrightLight example at example.com
Wed Jul 15 20:46:36 UTC 2026


On Monday, 6 July 2026 at 15:03:29 UTC, H. S. Teoh wrote:
> On Sun, Jul 05, 2026 at 09:57:55PM +0000, Meta via 
> Digitalmars-d wrote: [...]
>> > On 7/3/2026 4:08 PM, Dennis wrote:
>> > > In [The Easiest Way To Handle Errors Is To Not Have 
>> > > Them](https://www.dgtlgrove.com/p/the-easiest-way-to-handle-errors), Ryan Fleury gives concrete C examples, but the principles apply to other languages as well. I haven't read Walter's book recommendation "A Philosophy of Software Design" yet, and if you haven't either, maybe that post is more accessible.
> [[...]
>> This guy's mindset insane to me. Rather than have his program 
>> crash when it tries to dereference a null pointer, he wants to 
>> paper over it and keep going like nothing's wrong. A segfault 
>> or an assert triggering is the correct behaviour in this case; 
>> it's indicative that something has gone catastrophically 
>> wrong, such that execution cannot continue.
> [...]
>
> Then you're missing his point.  If you skim over his article, 
> it's easy to pick up the part about using nil structs or "fake" 
> pointers, but miss the other, equally important, part about 
> writing your code in such a way that *it's still correct when 
> passed a nil value*.  Without this second part, you immediately 
> run into all the problems that you mention.
>
> The whole point isn't only to substitute error paths with valid 
> (but nil) values; it is to structure your code so that it 
> handles both cases without bifurcating the code path.
>
> For example, if your function receives a buffer, then you could 
> either have it take a pointer (the typical C approach), or an 
> object that encodes the length of the buffer.  In the first 
> case, if the caller fails to allocate the buffer, you'd pass a 
> null pointer to indicate the buffer doesn't exist.  However, 
> doing that means you need a null check. Instead, you could use 
> the second approach: pass an object of zero length.  Then write 
> the function such that a zero-length buffer results in a no-op. 
>  Then when the caller fails to allocate the buffer, the 
> function does nothing (harmful), without needing a null check.  
> Note that you cannot ignore the second part -- if the function 
> wasn't written to gracefully handle an empty buffer, it might 
> do something totally wrong instead, like write to a dangling 
> pointer (I see this a lot in the C code that I work with: even 
> though a function may receive a buffer with length, the code 
> was written with the implicit assumption that the length is 
> non-zero, so when you pass in a zero length it malfunctions and 
> does something stupid).
>
> //
>
> Now, you mention that in some cases errors should not be 
> ignored, e.g. when you save a file and the operation failed.  
> Obviously, you don't want to just silently ignore the error in 
> that case.  The conventional approach is to throw an exception. 
>  The problem with that is that it bifurcates the control flow, 
> and most of all, these error paths are likely never tested.  
> (Tell me, when was the last time you wrote a unittest to check 
> that failing to open a file is handled correctly?  Or when the 
> disk is full and you try to write to a file?)
>
> Furthermore, these exceptional conditions often occur deep 
> inside the call stack, at some low-level utility function that 
> simply does not have the adequate context to know what to do 
> with the error.  So the only sane thing to do is to pass the 
> error state back up the stack and let some caller higher up the 
> call stack figure out what to do.  Throwing an exception is 
> typically one way of doing this.  However, then you run into 
> the problem of how a high-level function knows how to do: 
> because it may be so distant it has no idea that this low-level 
> function was even called, much less what kinds of exceptions it 
> might throw.  For example, you could have an I/O error in a 
> buffered write utility function.  It throws an exception -- but 
> the caller is an XML generator that's part of some library.  It 
> also doesn't know what to do with the exception, other than 
> propagate it, or return some error that XML write failed.  So 
> the error is pushed further up -- but the next caller is a 
> document writer module (XML is only a small part of the 
> document), which also doesn't know what to do, because it's 
> called by a function trying to save a backup file.  So it has 
> to pass the error along as well.  Then it turns out that backup 
> function is called by a script parser that's trying to save a 
> previous state before overwriting it with a new one. And the 
> script parser is called by a macro utility in some spreadsheet 
> application.
>
> Now consider the top level function, which is a user-action 
> handler processing a user command to edit a spreadsheet cell.  
> It has no idea that calling updateCell() can eventually call a 
> buffered I/O function that throws an IOError -- so it doesn't 
> even know to catch an IOError. And when the exception occurs, 
> how is it supposed to know what to do? The best scenario at 
> this point is to for it to display some generic error message 
> that editing the cell failed.  How is the user supposed to 
> understand why such an apparently simple action as inputting a 
> new value to a cell failed?
>
> //
>
> The proposed solution in the article is to keep a global error 
> log instead of throwing an exception.  So the I/O write 
> function would return a nil object (remember, we're assuming 
> that all the code, including its callers, are written such that 
> the nil object is handled correctly), but in addition, write to 
> a global error log.  At some point in the call stack, 
> presumably in the user-action handler, you'd want to know 
> whether the operation succeeded or not.  So that's where you'd 
> check whether the error log is empty -- if not, now you have a 
> log of what went wrong (I/O error -> XML write failed -> 
> document save failed -> backup state failed -> script failed -> 
> execute macro failed -> update cell failed), which can help the 
> user understand why the operation failed.
>
> Yes, you DO have to eventually check for errors -- but now you 
> can check for it only in a few places: in the actual low-level 
> function that failed and in the top-level function handling 
> user actions, instead of every level down the call stack (which 
> leads to 2^N bifurcating code paths).
>
>
> T

The issue is that our programs cannot always actually be in a 
valid, correct state (for some definition of correct state) when 
there is a nil struct or equivalent "None" value. Writing code 
that assumes None is acceptable from the point it is returned, is 
formally equivalent to throwing an exception with a catch-all at 
the start of the program. This first portion of the article feels 
like the author re-discovered the Maybe monad and is now excited 
about this "exciting, new way to handle the problem of errors" 
(the example they gave was SearchTreeForInterestingChain(Node 
*root)).

One way or another programs are going to have paths where there 
will be errors that must be handled. The quest to minimize these 
paths is noble and worthwhile, but we all know that it is 
impossible to "just don't have errors!".

The approaches we have in statically typed languages are as 
follows:

1. Return codes + conditional checks
2. Monadic approaches (sumtypes with then/bind chaining)
      -- Alternatively implicit monadic bind, as the author did in 
that function.
3. Exceptions
4. Algebraic effects

The approaches we have with dynamically typed languages are the 
same, except algebraic effects can instead be done far more 
simply as common lisp style Condition systems (some may know this 
idea as "resumable exceptions" even though it doesn't actually 
require an api resembling exceptions) (p.s. every dynamically 
typed language I touch, the first thing I do is implement a 
lisp-style condition system for my own use, including in D though 
that pigeonholes the code to rely on std.variant or Adam's `var` 
implementation)

In my opinion this entire space has already been mapped and 
solved; it's up to us to be familiar with them and take our pick 
depending on factors such as performance, and whether library 
code can handle the problems or if usercode needs to negotiate 
with library code on correct ways to handle problems.

Just don't use return codes.


More information about the Digitalmars-d mailing list