← Back to context

Comment by fluoridation

21 hours ago

>How exactly have you seen people use exceptions to "report errors" that is not for the purpose of indicating that "a contract cannot be fulfilled"?

Reporting normal errors to the caller, as opposed to exceptional errors. The distinction between normal errors and exceptional errors is kind of nebulous, but it boils down to normal errors being those that the immediate caller would be interested in, and everything else is exceptional.

For example, you have a file system class that abstracts away different underlying hardware interfaces to present to the client code a virtual file system. Not only would it be impractical for the open() function to include as part of its interface every possible error condition of all the possible backends, it would be of no help to the caller. If you're manipulating an AbstractFileSystem class just to open a path and read data, what could you possibly do with a connection reset error, or with a file system structure corruption error? Do you see what I mean? They're errors happening at different levels of abstraction. Exceptions are meant to be used to communicate error conditions when your call stack looks like

(low abstraction) ---> (high abstraction) ---> (high abstraction) ---> (low abstraction)

often with a module boundary between high abstraction stack frames. You use them to pass errors directly between frames at the same level of abstraction, that are not in direct communication.

So to answer your question, exceptions are misused when they communicate relevant errors within the same level of abstraction, such as an open() function throwing a FileNotFoundException.

>> How exactly have you seen people use exceptions to "report errors" that is not for the purpose of indicating that "a contract cannot be fulfilled"?

> Reporting normal errors to the caller, as opposed to exceptional errors. The distinction between normal errors and exceptional errors is kind of nebulous, but it boils down to normal errors being those that the immediate caller would be interested in, and everything else is exceptional.

...

> So to answer your question, exceptions are misused when they communicate relevant errors within the same level of abstraction, such as an open() function throwing a FileNotFoundException.

I think your thoughts feel a little bit jumbled here, possibly because you're oversimplifying things too much. Whether a caller would be interested in an exception doesn't really determine whether it should be an exception or an error code. For example, std::regex_error and std::format_error are very much of interest to the callers of the APIs that throw them (e.g., because the patterns user-provided and the callers expect potential failures) but that doesn't mean they shouldn't be exceptions. Or, for example, the only entity ever really interested in an std::out_of_range exception is the caller (say, catching it around a call like std::accumulate(i, j, [&](const auto& key) { return container.at(key); })).

You're right that those things all matter in a lot of cases, but the line isn't really drawn like that. IMO, the truth is that whether an exception should be raised vs. an error returned is really not so easy to pin down to a single factor. The things you mentioned matter, but so do run-time, compile-time, and development-time costs. The reason FileNotFoundException is better left as an error vs. an exception isn't so much that it's an expected error condition (in fact, some callers might expect it and others not), but because of the performance characteristics callers generally expect. You don't want disproportionate performance impact to those who need to open potentially non-existent files. If C++ exceptions were as cheap to throw as to avoid, then that would tip the balance significantly.

  • I think your examples don't help your argument.

    >std::regex_error and std::format_error are very much of interest to the callers of the APIs that throw them (e.g., because the patterns user-provided and the callers expect potential failures) but that doesn't mean they shouldn't be exceptions

    Most commonly, regexes and format string are passed from program data, not user input, so in the majority of cases I don't think it's correct to say that errors are an expected situation. Programmer error is definitely not something that can be handled at the call site, so it makes for a prime candidate to be signaled by an exception.

    >Or, for example, the only entity ever really interested in an std::out_of_range exception is the caller (say, catching it around a call like std::accumulate(i, j, [&](const auto& key) { return container.at(key); })).

    Why would you write exception handling code around that call instead of ensuring before the call that the indices are valid? If you did ensure that and an exception was still thrown, that's the kind of situation that the caller is not able to resolve, so why would it be interested in the exception? What could it possibly do to recover once it's been proven that the vector was changed concurrently?

    >If C++ exceptions were as cheap to throw as to avoid, then that would tip the balance significantly.

    No, I don't agree. Sorry for not making my position clearer. What I said previously assumes that you don't have any additional constraints beyond designing the best error handling possible. If you have technical reasons to avoid exceptions (they're too expensive, you're about to cross an FFI boundary, etc.) then that's a reason to stay away from them. I think you should use exceptions up to as much as I said. To use your metaphor, my argument is the floor under the plate of reasons to use exceptions, and the balance can only tip in the other direction.