The solution to "Error handling"...
H. S. Teoh
hsteoh at qfbox.info
Mon Jul 6 15:03:29 UTC 2026
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
--
Жил-был король когда-то, при нём блоха жила.
More information about the Digitalmars-d
mailing list