RFC: Change what assert does on error
Timon Gehr
timon.gehr at gmx.ch
Sun Jul 6 15:34:37 UTC 2025
On 7/6/25 15:54, Dennis wrote:
> On Sunday, 6 July 2025 at 02:08:43 UTC, Jonathan M Davis wrote:
>> If an Error is such a terrible condition that we don't even
>> want the stack unwinding code to be run properly, then instead of
>> throwing anything, the program should have just printed out a stack
>> trace and aborted right then and there (...)
>
> Yes! Hence the proposal in the opening thread.
> ...
It's a breaking change. When I propose these, they are rejected on the
grounds of being breaking changes.
I think unwinding is the only sane approach for almost all use cases.
The least insane alternative approach is indeed to abort immediately
when an error condition occurs (with support for a global hook to run
before termination no matter what causes it), but that is just not a
panacea.
>> I can understand not wanting any stack unwinding code to run if an
>> Error occurs on the theory that the condition is bad enough that
>> there's a risk that some of what the stack unwinding code would do
>> would make the situation worse, but IMHO, then we shouldn't even have
>> Errors.
>
> Yes! If it were up to me, Error was removed from D yesterday. But
> there's push back because users apparently rely on it, and I can't
> figure out why.
Because it actually works and it is the path of least resistance. It's
always like this. You can propose alternative approaches all you like,
the simple fact is that this is not what the existing code is doing.
> From my perspective, a distilled version of the
> conversation here is:
>
> Proposal: make default assert handler 'log + exit'
>
>> That's bad! In UI applications users can't report the log when the
>> program exits
>
> Then use a custom assert handler?
> ...
Asserts are not the only errors. I should not have to chase down all
different and changing ways that the D language and runtime will try to
ruin my life, where for all I know some may not even have hooks.
`Throwable` is a nice generic indicator of "something went wrong".
Contracts rely on catching assert errors. Therefore, a custom handler
may break dependencies and is not something I will take into account in
any serious fashion.
Also, having to treat uncaught exceptions and errors differently by
default is busywork. I have never experienced a situation where
unwinding caused additional issues, but I have experienced multiple
instances where lack of unwinding caused a lot of pain.
A nice thing about stack unwinding is that you can collect data in
places where it is in scope. In some assert handler that is devoid of
context you can only collect things you have specifically and manually
deposited in some global variable prior to the crash.
I don't want to write my program such that it has to do additional
bookkeeping for something that happens at most once every couple of
months across all users and be told it is somehow in the name of efficiency.
Also it seems you are just ignoring arguments about rollback that resets
state that is external to your process.
>> I do, but the compiler needs to ignore `nothrow` for it to work
>
> What is your handler doing that it needs that?
>
>> log + system("pause") + exit
>
> Why does that depend on cleanup code being run?
> ...
Because it is itself in an exception handler that catches Throwable, or
in a scope guard. Whatever variables are referenced in "log" are likely
not available in a hook function.
>> ...
>
> And this is where I get nothing concrete, only that 'in principle' it's
> more correct to run the destructors because that's what the programmer
> intended. I find this unconvincing because we're talking about
> unexpected error situations, appealing to 'the correct intended code
> path according to principle' is moot because we're not in an intended
> situation.
> ...
The language does not have to give up on its own promises just because
the user made an error. It's an inadmissible conflation of different
abstraction levels and it is really tiring fallacious reasoning that
basically goes: Once one thing went wrong, we are allowed to make
everything else go wrong too.
Let's make 2+2=3 within a `catch(Throwable){ ... }` handler too, because
why not, nobody whose program has thrown an error is allowed to expect
any sort of remaining sanity.
> What would be convincing is if someone came forward with a real example
> "this is what my destructors and assert handler do, because the cleanup
> code was run the error log looked like XXX instead of YYY, which saved
> me so many hours of debugging!".
It's not about saving hours of debugging, it's getting information that
allows reproducing the crash in the first place.
I don't actually write programs that crash every time, or even crash
frequently. I want to reduce crashes from almost never to never, not
from frequently to a bit less frequently.
As things stand, I just save the interaction log in a `scope(failure)`
statement, on the level of unwound stack where that interaction log is
in scope.
It is indeed the case that I have not ran into an issue with destructors
(but not `scope(exit)`/`scope(failure)`/`finally`) being skipped in
practice, but this is because they were not skipped. It caused zero
issues for them to not be skipped.
Adding a subtle semantic difference between destructors and other scope
guards I think is just self-evidently bad design, on top of breaking
people's code.
> But alas, we're all talking about
> vague, hypothetical scenarios which you can always create to support
> either side.
> ...
I am talking about actual pain I have experienced, because there are
some cases where unwinding will not happen, e.g. null dereferences.
You are talking about pie-in-the-sky overengineered alternative
approaches that I do not have any time to implement at the moment.
Like, do you really want me to have to start a separate process, somehow
dump the process memory and then try to reconstruct my internal data
structures and data on the stack from there? It's a really inefficient
workflow and just doing the unrolling _works now_ and gives me
everything I need.
For all I know, Windows Defender will interfere with this and then I get
nothing again.
>> And maybe we should make the behavior configurable so that programemrs
>> can choose which they want rather than mandating that it work one way
>> or the other
>
> Assert failures and range errors just call a function, and you can
> already swap that function out for whatever you want through various
> means. The thing that currently isn't configurable is whether the
> compiler considers that function `nothrow`.
> ...
Yes, these are functions. They have no context.
> The problem with making that an option is that this affects nothrow
> inference, which affects mangling, which results in linker errors. In
> general, adding more and more options like that just explodes the
> complexity of the compiler and ruins compatibility. I'd like to avoid it
> if we can.
>
We can, make unsafe cleanup elision in `nothrow` a build-time opt-in
setting. This is a niche use case.
More information about the Digitalmars-d
mailing list