It's also true that with more experience, we've figured out that cleanup code is special. When I throw an exception during normal processing, I want to abandon what I'm doing. But if I discover that one step of a destructor cannot be completed, I do not want to abandon the rest of the cleanup. If I throw an exception in a destructor, I can't come back later to finish any remaining steps.
And, as I asked earlier, if I throw an exception because I can't close a file, what do I expect the exception handler to do? I guess it could try closing the file again, but I'm not going to hold my breath on that working. Perhaps the file is on a network share and the network's gone down. Perhaps closing the file will involve writing some cached data to disk, but the disk is full. In my experience, in this case your only real options are "ignore it, either because there's nothing you can do or because you're already dealing with an exception and you've simply discovered another symptom of the original problem" or "kill the program because there is no hope." You can log the fact that a step failed, but remember that it's possible for logging to fail, if the disk is full.
In my experience the exact opposite is true. Error handling or "cleanup" code is in no way special. Errors are not exceptional and there is nothing we can say in general about what error handling code will or will not need to do.
Error handling code needs to be able to use the entire language and reuse all the regular code that is used elsewhere. A language should not have two modes, an error mode and a normal mode with completely different non-local semantics.
>And, as I asked earlier, if I throw an exception because I can't close a file, what do I expect the exception handler to do?
That depends entirely on the context of the program you're writing. It might want to do things like writing a record to a database that marks the file as invalid/corrupt. It may need to notify some other system about the failed action. It might want to roll back a transaction or initiate a compensating transaction. It may want to abort some session and schedule it to restart at a later time.
There is simply nothing we can know in general about what a caller of an action that failed to complete cleanly may want to do as a result of that failure.
Using exceptions for error states that may require a complex response is just not a good idea and in C++ it's an even worse idea.
[Edit] But to be clear, my initial thought wasn't about exception handling but rather about ubiquitous use of RAII for all sorts of stuff that needs to get done at the end of a scope. Much of that has nothing to do with errors at all. Maybe if you think of these situations it will become clearer why I think that arbitrary limitations of code that runs in a destructor is problematic. C++ has no finally block. Destructors is all we have.
I agree with you on that. It turns out that, say, having a lock guard automatically release a lock when it falls out of scope can cause a ton of trouble when it falls out of scope in a place the programmer overlooked.
Classic example is logging an error on failure. This means calling a logging function in the catch block, and then letting the exception propagate. But what if the call to the logging function fails? In Java, coded naively you'd simply drop the original exception. Usually that's not what you want.
You can examine the issue with error codes, it's not any better.