Hacker News new | ask | show | jobs
by kibwen 608 days ago
The workflow you're describing is also how it works in languages without resumable exceptions, except that you're forced to acknowledge when a function call is capable of producing an error. Whether you want to ignore the error, handle the error, or propagate the error, it's all the same; you're just required to be explicit about which approach you're taking (as opposed to exceptions, where the the implicit default is to propagate).
5 comments

Indeed. While it is painful for the people who know they have a simpler architecture, making errors and other cross-cutting effects explicit is necessary at some point. It's essential complexity that shouldn't be hidden; it should be addressed from the get-go. Although the industry largely has the wrong incentives and discourages robust, comprehensible programs.
My belief is that errors should be handled using effect systems. So in the signature but not muddying the actual return types.

Useful effect systems allow the end user to decide where and when to have the compiler enforce errors are handled. Nim has had an effect system for a while but became much more useful when `forbids: [IOError]` was added. It makes it easy to ensure certain type of errors are handled at specific points.

More languages should embrace effect systems. Ocaml's is even used to implement multithreading support, albeit effect systems vary widely in design and theory.

I mean this is how a lot of exceptions are handled, even in C++. You can use noexcept and whatnot and you don't have to change types and propagate them out. In Rust, you do. Java has maybe the strongest system because not only do you have to declare what can throw but it checks it at compile-time. That's, to me, a full featured effect system.

But errors-as-values are all the rage today. But modifying types, especially every type in the chain, is annoying and overly manual IMO.

Yeah, Java had some good ideas but just not quite there on the UX, like many things with Java sigh.

Checked exceptions were annoying because you had to manually annotate the exceptions all the way up your chain. The list of effects should be generated by the compiler, and the IDE should show them when desired. Maybe manually annotated at external API boundaries which double as forcing the API dev to handle unlisted exceptions.

> except that you're forced to acknowledge when a function call is capable of producing an error

Acknowledging the error is handling the error even if partially. The point of exceptions is to only acknowledge error that one can/knows how to properly handle

> The point of exceptions is to only acknowledge error that one can/knows how to properly handle

In Rust this takes a single character. There's effectively no cost to having the programmer acknowledge the error, and there's a large benefit in that you now know that there's no such thing as an error that the programmer ought to have handled but was simply unaware of. That's a huge benefit for writing resilient software.

It's one character in the best case. In the worse case you need to convert the error types to your error type which just re-wraps or replicates the upstream error type. Then repeat this for every library type you use. Zigs error design seems saner in this aspect at least. Rust, IMHO, just makes errors require lots of unnecessary manual labor instead of being smart about it.

Alternatively everything just gets put into a `dyn trait` and you're effectively just bubbling up errors just like with exceptions, but with way more programmer overhead. The performance overhead of constantly doing if/else branches for errors adds up as well in some situations.

Of course a fair bit of Rust code just uses `unwrap` to deal with inconvenient errors.

One thing I’ve wondered about is, isn’t the cost of checking for the failure case in the good case all the time actually worse (even if only slightly) than the cost of not throwing, which is nothing?
There's indeed a predictably present cost for checking for failure all the time. Exceptions, depending on the implementation, often do come with runtime overhead too. If the determining factor is a slight performance gain of exceptions over ubiquitous checking, that would be an exceptional (ha) case. I daresay there are almost always other more salient factors, if harder to rearchitect around.
AIUI most implementations of exceptions only carry a cost on unwind, but I'm not the expert here.
> you're forced to acknowledge when a function call is capable of producing an error

All functions are capable of producing an unbounded set of errors.

(Yes, programming is hard.)

>you're just required to be explicit about which approach you're taking

Yes and I'm opposed to such "if err != nil" boilerplate.

> Yes and I'm opposed to such "if err != nil" boilerplate.

That is not what Rust boilerplate looks like.

I'm aware. One of the criticisms often leveled against Go is that it's needlessly verbose when handling errors, which is why I chose that example.
And you're not opposed to try-catch boilerplate?
Boilerplate is code you have to write almost as a pro forma thing. If (in go lang, to continue my example) you're just going to keep copy pasting the same if statement to return `err` up to some higher caller, then why write all those lines when at the top level a single try/catch can remove potentially dozens of lines of code?