Hacker News new | ask | show | jobs
by fluoridation 3 days ago
Exceptions aren't meant to report errors, just in general. That's a misuse of them. Exceptions are meant to be thrown when a contract cannot be fulfilled. Yes, you're unable to know what exceptions a function may throw. That's the way it should be, because exceptions aren't supposed to be part of the function's contract.

For example, you're implementing an arithmetic operator and have reached an erroneous state, but the arithmetic type doesn't have an error value, the only way to communicate the error is by throwing. Another example: you've specified that a function must always succeed, but later on you find a case where the function cannot succeed. Instead of fixing all the possible call sites, throw an exception. All those callers could not have handled the error anyway, because they were coded under the assumption that no error would happen at that point. Throwing an exception and letting it unwind the stack way up (perhaps even all the way up to main()) is the sensible solution, because at that point you've reached a situation with no reasonable way for that code to handle.

Saying that you prefer Result over exceptions is like saying that you prefer strings to functions. They do different things. If you like Result, nothing prevents you from implementing a C++ equivalent.

2 comments

> Exceptions aren't meant to report errors, just in general. That's a misuse of them. Exceptions are meant to be thrown when a contract cannot be fulfilled. Yes, you're unable to know what exceptions a function may throw. That's the way it should be, because exceptions aren't supposed to be part of the function's contract.

I don't think these are true? What about std::vector::at(), std::optional::value(), etc.? And then there's std::system_error.

>std::vector::at(), std::optional::value()

Both functions must return T &. If the vector is not long enough, or the object is not set, then returning a T & is impossible. So we have a function that has already been called and which must return something valid, and cannot return something valid. The only two ways to resolve this contradiction is to throw, or to terminate.

(Well, you could also trigger undefined behavior like operator[]() and operator*(). No comment.)

>And then there's std::system_error.

And what am I supposed to conclude from the existence of a type?

> The only two ways to resolve this contradiction is to throw, or to terminate.

Or they’re bad APIs that should be redesigned to be not bad.

They’re fallible functions. Don’t write fallible APIs that require exceptions to report errors! That’s bad API design!

I think you missed my point. I was referring to the fact that some of these standard exceptions are very much a part of the contracts of their respective functions. In fact, that's their entire point. This directly contradicts what you wrote.
You're using "contract" in a different sense than I did. When I said "contract" I was referring to the required state of the program when the function is called and the guaranteed state of the program when the function returns. By definition an exception cannot be part of the contract in this sense, because a call that throws does not return. This narrower sense of contract is critical, because the entire point of exceptions is to enable alternate control path when it'd otherwise be impossible, such as in the examples I gave above with overloaded operators and code with evolving requirements.
Okay, but if I may offer a suggestion: in the future, I would probably phrase what you said differently. I think it caused you trouble here not just with me but with other readers. Instead of "when the function cannot fulfill its contract", I would talk about "when returning would not fulfill a function's contract."

There are actually two reasons for this, not one:

- It violates standard terminology. The notion of a contract/postcondition/etc. is not specific to C++ or the particular implementation's mechanisms for exiting a function. It simply means a condition that must hold true after the execution of some piece of code. [1] The intent of this definition is to allow program composition: it enables one to reason about the greater program in terms of the sub-parts. Defining it to be anything else just throws people off, and rather misses the point and utility of the term.

- "Cannot" is actually too strong. A function might, in fact, be able to fulfill its contract, but still choose not to. An easy example is something like a constraint solver (SAT, chess, simulator, constexpr evaluator, or whatever). It's guaranteed to be able to find the solution eventually if it keeps going, but that's probably not always a good idea.

Now, going back to what you wrote here:

> Exceptions aren't meant to report errors, just in general. That's a misuse of them. Exceptions are meant to be thrown when a contract cannot be fulfilled.

I'm still not entirely sure I see what you mean by "report errors". 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"? The description makes it sound like using exceptions for the purpose of logging, but that would seem like a strawman... I have never seen anyone write throw instead of log. What are you referring to?

[1] https://en.wikipedia.org/wiki/Postcondition

>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.

> Throwing an exception and letting it unwind the stack way up (perhaps even all the way up to main()) is the sensible solution

No. I would never in a million years do this.

If the API is that a function is infallible and then I decide that it’s a fallible function then that’s a pretty major change and I’m just gonna have to update all the call sites to deal with a fallible return result.

Saying throw an exception and bubble up to main provides just about zero value. Might as well just call std::abort. Which is also something I would never do.

> Saying that you prefer Result over exceptions is like saying that you prefer strings to functions. They do different things.

So here’s the thing. In 20+ professional years as a C++ dev I have never ever once worked in a codebase where exceptions were used. Certainly never in first party code. Only when dealing with annoying thirdparty libraries that leveraged them.

I think your comment “contract can’t be fulfilled” is cheating. No. You’ve simply made a new contract and the new contract is that under certain cases an error is returned in the form of an exception.

> Saying throw an exception and bubble up to main provides just about zero value. Might as well just call std::abort. Which is also something I would never do.

I'm sorry but this is where you're clearly wrong. The whole point is unwinding doesn't have to necessarily happen all the way to main(); there is a ton of value in doing this, and it is not at all equivalent to aborting. It lets someone in the call chain do something other than abort, or clean up stuff that they otherwise might not have a chance to. Like logging an error, telling the client there was an internal error, dumping additional information that wouldn't be useful in the successful case, and/or moving on to another task. All gracefully, without any intermediate functions having to care (aside from providing basic exception safety), and without having to throw your hands up and give up. Aborting without being asked is rather presumptuous and robs your callers of all opportunities to do anything about the problem you encountered.

People do this stuff and find it useful... you're effectively telling them all that they're doing something useless and they may as well just abort. That's... quite a claim.

>If the API is that a function is infallible and then I decide that it’s a fallible function then that’s a pretty major change and I’m just gonna have to update all the call sites to deal with a fallible return result.

What if you don't control those call sites?

>Might as well just call std::abort.

Sure. I mean, not really, because the caller cannot handle an abort. You're making a decision for the caller that the situation is unresolvable, where the caller might disagree.

>No. You’ve simply made a new contract and the new contract is that under certain cases an error is returned in the form of an exception.

If the function doesn't use exceptions for normal error conditions, then no, it's not a new contract, because you don't need to do or know anything specific to handle the situation. You could do something like

  void transaction(){
    try{
      //...
      commit();
    }catch (std::exception &){
      rollback();
    }
  }
and not have to worry about the specifics. It's just an exception. You don't have to care about what exactly happened, you just care that something that couldn't be resolved happened. When exceptions are misused you see stuff like

  try{
    some_function();
  }catch (SomeErrorConditionSpecificToSomeFunction &){
    //...
  }
Not always, but this does usually mean that the exception is part of the contract of the function. It's a condition that the caller must handle as part of the normal usage of the function. FileNotFound exceptions are quite often a prime example of exception abuse.

Replying to your other comment here:

>They’re fallible functions. Don’t write fallible APIs that require exceptions to report errors! That’s bad API design!

I disagree. You should ensure your arguments are valid before indexing vectors and dereferencing optionals. You wouldn't iterate a vector like this, I imagine?

  for (size_t i = 0;; i++){
    auto x = vector.at(i);
    if (!x.has_value())
      break;
    //...
  }
> What if you don't control those call sites?

If I am choosing to change the API contract then someone who wants to use the new API has to update. This is not a big deal.

> If the function doesn't use exceptions for normal error conditions, then no, it's not a new contract

I disrespectfully and emphatically disagree. I do not accept your definition of contract.

> You could do something like (try-catch wrapper)

Let me be clear. Having to add a bunch of random fucking try-catch bullshit around every fucking function call is EXACTLY why I hate exceptions and is EXACTLY what I think is bad software design.

If you think a function should return a value or some unspecified exception whose details are irrelevant then that function could return an option with no information loss, or a result with an Error that is ignored.

> You wouldn't iterate a vector like this, I imagine?

I wouldn’t use at(i) for iteration. The only reason for a function like at(i) to exist is because you want it to be fallible.

> Let me be clear. Having to add a bunch of random fucking try-catch bullshit around every fucking function call is EXACTLY why I hate exceptions and is EXACTLY what I think is bad software design.

The whole point of exceptions is that you don’t need to handle errors at every call site. You can just have one central try-catch block at a place where you have a way to deal with the error, such as report it to the user.

> having to add a bunch of random fucking try-catch bullshit around every function call

Not the person you are replying to, but did you see where he said:

> When exceptions are misused

That’s a No True Scotsman argument. It is not a good or useful one imho.
>If I am choosing to change the API contract then someone who wants to use the new API has to update. This is not a big deal.

Throwing lets you handle the new situation without changing the API at all.

>Let me be clear. Having to add a bunch of random fucking try-catch bullshit around every fucking function call is EXACTLY why I hate exceptions and is EXACTLY what I think is bad software design.

See, that's what happens when you form your opinions on half-digested ideas. Let me be clear. You don't add "a bunch" of try-catch blocks. You don't wrap every call that's capable of failing exceptionally in a try-catch block. That's exactly how you don't use exceptions. The whole point of exceptions is that the compiler will handle the stack unwinding for you so you don't need to worry about it. If you don't want to, or don't know how, or can't handle an exception at a specific point then don't. Let it bubble up for someone else to catch. See the ellipsis in my example? Inside of it you might have a gigantic call tree that performs all sorts of different operations that may all fail in different and unexpected ways. You could write the whole thing and not have a single try-catch besides the one I wrote explicitly. Let me reiterate; this is what you DON'T do:

  try{
    foo();
  }catch (...){
    return Error1;
  }
  try{
    bar();
  }catch (...){
    return Error2;
  }
  try{
    baz();
  }catch (...){
    return Error3;
  }
The only reason you would do something like this is to satisfy a specification such that you have to return different errors, specifically when each of the different calls fails. So... don't specify your functions such that you're required to do this? Just do

  foo();
  bar();
  baz();
or if you really must not throw from the function,

  try{
    foo();
    bar();
    baz();
  }catch (...){
    return SomethingFailed;
  }
TL;DR: Instead of bitching about exceptions, learn how to use them properly.
> Throwing lets you handle the new situation without changing the API at all.

I do not disagree. https://xkcd.com/1172/

If you add exceptions to a library that didn’t previously use them then I almost definitely have to update my code. The fact that it compiles and runs but will behave in undesirable ways makes it even worse, not better!

> or if you really must not throw from the function,

I’m aware.

But if your library that offers foo adds exceptions now I need to think about it at every single callsite, and probably wrap the function. It’s extremely irritating.

> learn how to use them properly.

In my 20+ years of professional C++ development I have a great experience not using exceptions and a strictly negative experience using them.

Perhaps sometimes I’ll stumble upon a library or codebase where exceptions make the code simpler, easier to understand, and easier to write. But my experience is exceptions make everything strictly worse and that not using exception is a strict win with zero downsides.

>The fact that it compiles and runs but will behave in undesirable ways makes it even worse, not better!

* Exceptions

* Unstable API

* Incorrect behavior

Pick your poison. I know what I prefer.

>But if your library that offers foo adds exceptions now I need to think about it at every single callsite

You really don't. Like I said, it's kind of the whole point of exceptions.

>In my 20+ years of professional C++ development I have a great experience not using exceptions and a strictly negative experience using them.

Hence my recommendation to learn how to use them. I can replace "exceptions" with anything else (computers, diesel engines, HR people), and there's probably someone who holds that belief. That doesn't make it true.

And GTFO with that No True Scotsman nonsense. That a tool can be misused doesn't delegitimize the tool. If you saw someone using classes instead of namespaces you wouldn't conclude classes are bad, you'd call that person a knobhead.

> If you add exceptions to a library that didn’t previously use them then I almost definitely have to update my code.

No, that's the whole point. You let them bubble up to the top of the event loop and you report the error to the user. As a user, anything else leads to shitty software where the programmer tries to outsmart the world around them (and fails, obviously, leading to worse end-user experience than just admitting that you don't and can't have control over everything)