Hacker News new | ask | show | jobs
by SuperV1234 643 days ago
This article feels like someone trying to find arguments and justifications in favour of an existing opinion/bias.

> Compare this to functional-style errors, where error handling is manual and super tedious. You have to explicitly check if the return value is an error and propagate it.

No, you don't. You can easily convert a functional style error into an exception on the call site explicitly (e.g. `std::optional<T>::value` in C++, or `.unwrap()` in Rust), propagate via language features (e.g. `?` in Rust) or library abstractions such as monadic operations.

The fact that you explicitly have to check the return value is a massive win in readability and ensuring that the "error case" was considered by the caller. That is paramount for the robustness of any codebase.

> But there’s much more: Allocations can fail, the stack can overflow, and arithmetic operations can overflow. Without throwing exceptions, these failure modes are often simply hidden.

Turns out there is a good use case for exceptions: exceptional and rare errors that cannot be reasonably handled in the immediate vicinity of the call site.

Exceptions and error types can and should coexist nicely.

> The classic example is syscalls, which usually follow C conventions.

Yes, C error handling from decades ago sucks at providing information and context for the source of the errors. This is not an intrinsic issue with error return types, it's just another thing that sucks about C and is the way it is because it's old.

> We parse an int somewhere, and an `IntErrorKind::InvalidDigit` bubbles up at the user.

How is that a bad thing? Either the user provided the string that should have been parsed, therefore it's useful information for them to know why it failed to parse, or they don't care much, and they can explicitly decide to convert the error into an exception and propagate it upwards.

Again, it forces the user to think about the error case, which is excellent!

> The following examples use C++ code, which allows us to compare both versions like for like: [...]

Now show a benchmark where the error rate is 50% or more.

2 comments

> C++ code ... Now show a benchmark where the error rate is 50% or more.

C++'s policy of "exceptions should be exceptional" isn't a good answer to this either, and it introduces a lot of ugly edge cases when writing code. For instance, you have to make the judgment call of whether to handle commonly-expected errors as return values or exceptions (eg. checking if something exists, and returning it if it does).

In many cases it is impossible to make the right call. For instance, a "file not found" exception is no big deal when your library is being used in a GUI app where user actions are relatively infrequent, but if your library is used in a high-traffic server or for batch processing, suddenly all those "file not found" exceptions are eating a lot of CPU.

In general, you simply can't predict when users of your C++ code might find a case where a code path spams exceptions, tanking performance.

This problem also exists in other exception-using languages like Python and C#[0], but these languages tend to be used for less performance-intensive purposes (especially Python) so it doesn't come up as much, and generally exceptions are used even for expected errors.

[0] https://stackoverflow.com/questions/891217/how-expensive-are... (C# exceptions are 30,000 times slower than return codes)

Some of the details in the linked SO post are 15 years old(!).

Performance today is a very different story :)

With that said, exceptions are still very expensive and cost about 6-7us in .NET 8 and 3-4us in upcoming .NET 9. The cost will grow if the catch/catch with filter/multiple finally blocks are present and need to be executed up the call stack.

When it comes to I/O, the cost of "file no found" exception will be minuscule comparing to reaching to OS I/O stack. Luckily, many codebases today adopt one of the community packages to expose potentially failing operations with Result<T, E> instead. For some reason, the highest adoption is in the line-of-business code. Lower-level codebases often just do just Try* or T? patterns instead.

> 6-7us in .NET 8 and 3-4us in upcoming .NET 9

That's still roughly 7000-3000 times slower than a plain return. I'm not convinced the extra overhead of an entirely separate control flow mechanism will ever be worth it in any language that cares about performance. Especially since I haven't heard many complaints about the way Rust does things. And there's still room for new idioms, syntactic sugar, and tweaks to the type system to optimize the convenience/rigor of that model.

> When it comes to I/O, the cost of "file no found" ...

That was just an example. Maybe that code is just a read from a (cached in memory) sqlite db, or a "value out of a range" error before some math operation, in which case the exception can easily be the most expensive part, especially if all the exception junk dirties up the CPU cache and branch predictor.

> Some of the details in the linked SO post are 15 years old(!).

Sigh. Thanks for the correction. You'd think I would have a habit of checking the post date by now!

> That's still roughly 7000-3000 times slower than a plain return. I'm not convinced the extra overhead of an entirely separate control flow mechanism will ever be worth it in any language that cares about performance. Especially since I haven't heard many complaints about the way Rust does things.

Ah, I did not mean to make a case for or against exception-based error handling. Only that it's not as expensive anymore, and that other operations do not necessarily use it: int.TryParse, dict.TryGetValue, encoding.TryGetBytes, etc.

I think the way Rust does it with Result<T, E> and, most importantly, implicit returns is the error handling perfected. You can criticize the language for being on the more verbose side, especially if you are not writing an OS kernel or a driver, but in terms of error handling while preserving the richness of error state it is by far the best per LOC.

One thing to note is, as usual, the Rust style error handling is not free either and you end up paying with at least a single branch in a happy path for each call that may error out provided it's not inlined and error check is not optimized away, and the additional codegen that is needed for blocks that e.g. construct a particular error and return, something that exception handling does "outside" of regular executed code (yes, with disproportionately higher cost but still).

If the invalid int came from the network it could be confusing to tell the user