Phobos 3 Discussion Notes - 02-01-2024

Jonathan M Davis newsgroup.d at jmdavisprog.com
Sun Feb 4 12:35:02 UTC 2024


On Sunday, February 4, 2024 3:13:39 AM MST ryuukk_ via Digitalmars-d wrote:
> EH is evil, some platforms has banned it completly (apple), and
> microsoft is all in rust, they are rewritting their C#
> application in Rust too, not just C++, it is an inferior design
>
>
> https://www.theregister.com/2024/01/31/microsoft_seeks_rust_developers/

A number of us do not agree with you, and I for one never will. For many
situations, exceptions are by far the best error handling mechanism that
there is. D's implementation of it could certainly use some improvements
(e.g. while being able to have a hierarchy for exceptions is valuable, the
fact that they're classes and thus have to be heap allocated is a problem
for code that needs to avoid the GC), so it would be great if D's
implementation for exceptions could be improved, but the basic concept is
solid and makes a lot of code cleaner as a result (e.g. I'd never want to
write a parser that didn't use exceptions; being able to separate the error
handling code from the actual parsing makes the code _far_ cleaner and far
less error-prone than any other error handling mechanism that I've ever
encountered).

Obviously, there are situations where exceptions are not appropriate (e.g.
it was definitely a mistake to have Phobos decoding Unicode all of the place
and potentially throw a UTFException from practically anywhere as a result),
and any properly written library is going to have to be intelligent about
when any particular error handling mechanism is used, but exceptions are
far, far too useful to not use - especially when the main alternative is
checking return values all over the place. So, while there are places that
Phobos currently uses exceptions where it shouldn't, there are others where
using them is very much the right thing to do IMHO, and I would use them
again in the next version of Phobos (and argue strongly against anyone
trying to not use them there), because if I didn't, the result would be
worse code that was harder to use.

In general, exceptions are best when

1. It's cleaner to assume that an operation will succeed, and it's
reasonable to assume that it will succeed. Parsing is a great example of
this. You're not going to do something like validate that an XML document is
going to parse correctly and then parse it, since that would basically mean
parsing it twice. You're just going to parse it and then report when that
fails so that the caller can handle it. And exceptions make that very clean,
because then you only have to have the one place at the top that catches the
exception, and the rest of the code can just do its thing. The way that
exceptions bubble up, allowing code along the way to completely ignore the
error handling and then have somewhere higher up the stack that is actually
in a position to handle the error condition catch the exception and handle
it is an amazing feature.

2. It's also best to use exceptions when you can't guarantee that an
operation will succeed but where it's reasonable to assume that it will -
particularly when it's actually impossible to check that an operation will
succeed beforehand. A great example of this would be reading a file. It's
good practice to check that the file exists first, but even then, you can't
guarantee that opening it will succeed, because the file could be removed
between the time you check and the time you go to open it (or a variety of
other errors could occur which cause opening or reading the file to fail).
The same goes for most file operations really. Any of them could fail, and
there needs to be a way to report that, and it clutters up the code
considerably if you have to deal with checking return values all over the
place - on top of the fact that it makes it impossible to simply return an
object or buffer from a function and use it if you also have to check an
error condition. You're stuck either returning the object via one of the
parameters or using a compound type where you have to check if the operation
succeeded and then extract the actual return value from it, which absolutely
destroys your ability to chain function calls - on top of making it far more
likely that code will fail to check the return value like it should,
resulting in it assuming that the call succeeded when it didn't.

3. For constructors, exceptions are really your only option unless you want
to get into doing two-part initialization, which is known to be error-prone,
and the advice I've almost always seen is to avoid it like the plague. So,
if a constructor needs to validate its arguments - or report any other error
condition that might occur while it's being constructed - an exception is
going to be the way to do it unless you want to do something like add a
member function to the type just so that you can check whether it was
constructed correctly, and at that point, you might as well just be doing
two-part initialization.

4. And exceptions are an excellent choice in any situation where you need to
be sure that an error condition is not ignored - or at least put the code in
a position where either the exception is caught and handled in whatever
manner is appropriate, or the program is killed, because the error condition
wasn't handled. Obviously, not all error conditions fall into that category,
but there are plenty of situations where error conditions must be handled
for the application to operate properly, and that's much more likely to
happen with an exception than with any other error reporting mechanism that
D has. Some other languages force that by forcing you to check return
values, but that can be incredibly annoying, and it results in far more
verbose code, because you have to put error-handling code everywhere instead
of just letting the exception bubble up to the code that can actually handle
it properly.

Now, obviously, there are situations where exceptions aren't appropriate. An
obvious one is where an error is likely, because using an exception in that
kind of situation is going to be inefficient in comparison to other options,
and if the error path is that likely, then trying to separate out the error
handling code like you do with try-catch statements really doesn't make
sense anyway.

And of course there are situations where validating stuff ahead of time is
perfectly reasonable and allows the code beyond that to just assume that
everything works without needing to report error conditions at all, because
the validation was already done. Unicode decoding is one such case where
that often makes sense. If you validate the Unicode when you read in a file,
then the rest of the code doesn't have to. Phobos, unfortunately, currently
picks the worst of both worlds, because readText checks the Unicode, and
then you have auto-decoding all over the place which validates the text
again and throws an exception if it's invalid. So, we end up with a bunch of
code that can't be nothrow or @nogc just because of a potential UTFException
which can't possibly be thrown if the text was already validated.

And of course there are also situations where it's actually reasonable to
ignore an error, in which case, throwing an exception wouldn't make sense -
but no other reporting mechanism would either. Unicode can actually be
handled in such a fashion in some cases, because it has the replacement
character so that you can use it anywhere that you encounter an invalid code
point, which gives text processing a way to process text without worrying
about invalid Unicode. That's obviously not a good choice in all cases, but
without auto-decoding, that choice is left up to the programmer as it should
be.

And with D, there are situations where exceptions can't be used where they
could be in a language like C++, because D's exceptions are allocated on the
heap, which not only means allocating memory, but it typically means using
the GC, which is fine for most code but not fine for all code. So, anyone in
that kind of situation is going to need to avoid exceptions even if they
would otherwise be the best choice.

And naturally, there are idiots who use exceptions for stupid things (e.g.
throwing an exception when you're checking a condition instead of just
returning bool, or having a type implement an interface and then throw an
exception if you call a function that it doesn't actualy support), which I
expect is part of the reason that some folks dislike exceptions. So, there's
no question that exceptions can be a problem, but overall, in the hands of
someone who knows what they're doing, they can be an excellent tool and
result in much cleaner, less error-prone APIs.

Obviously, you're free to hate exceptions for whatever reasons you may have,
and we're obviously not going to use them for everything in Phobos, because
that would be dumb, but given that there are cases where code is clearly
cleaner and easier to use correctly when exceptions are used, you're not
going to find general agreement with the idea that exceptions are inherently
bad. I expect that for most people, the disagreement is going to be on which
cases exactly they make sense for, and which they don't, not whether they're
a bad idea in general.

And personally, I wouldn't use D if it didn't have exceptions. It would be
far too miserable to not have them. So, while D's exceptions could certainly
use some improvement, I never would have started using D if it hadn't
supported exceptions, and if Walter seriously tried to get rid of them, I'd
go find another language to use instead. The exact form exceptions in D take
may change at some point in the future, but their basic functionality is
IMHO critical to writing good APIs, and part of the reason that languages
like C are miserable to use is that they don't have a comparable feature.

- Jonathan M Davis





More information about the Digitalmars-d mailing list