Hacker News new | ask | show | jobs
by xg15 1755 days ago
No, I don't want to wrap every single statement of my program in its own if-block, thank you very much.
6 comments

Rust solves this issue by having a ? operator to bubble up Errors. Before that there was the try! macro with the same semantics. That cuts the boilerplate to a minimum while having a well defined and explicit control flow.

I agree that if you had to write the ifs by hand it would be a pita. Looking at you, Go.

In the end that is equivalent to bubble up exceptions when thy are of the unchecked type.
I don't think that's true because if I understand it correctly, the return type of functions which can possibly throw unchecked exceptions would not indicate that they can throw or what they can throw. On the other hand, with the "errors as values" approach (including "bubbling up" operators like `?`), you can tell exactly from the function's return type if an error can be returned and if so what the set of possible errors is.

Did you maybe mean "the checked type"? In that case I still think it's not equivalent because at least in Rust you can automatically transform the error while it bubbles up, while I don't know of a language with checked exceptions that lets you transform the exception while unwinding (short of manually catching, transforming, and re-throwing).

> the return type of functions which can possibly throw unchecked exceptions would not indicate that they can throw or what they can throw

As far as I know, that's how Java's "throws" method signature works, which has been widely regarded as a mistake.

Throws is for checked exceptions. Unchecked are not listed in the throws list.
It's actually even worse: you can include both checked and unchecked exceptions in the `throws` clause. The compiler will only enforce handling of the checked exceptions included. Unchecked exceptions listed in the `throws` clause serve as an optional hint to others. Note that you can erroneously include any exceptions you like in the `throws` clause, even ones that are never thrown from the method. These quirks are often covered by static analysis.
The key difference for me is that you have to explicitly handle Results somehow, you can't just pretend that the function is infallible and hope that something up-stack is going to deal with all the failure modes. Also while you can have generic Error types it's generally frowned upon for libraries which are encouraged to provide meaningful error types that can be used to decide how a problem should be dealt with. Coupled with Rust's pattern matching it makes for concise and expressive error handling in my experience.
One could solve the problem further and more conveniently by doing the bubble up implicitly at every call and just re-invent exceptions.
If you have to do this, the structure of your program might be wrong.

Don't check the return value of a call to the same API multiple times. Make it such that all calls to the API go through the same code that you write, so you have to check the return value only "once". This may sound extreme, but it's pretty close to what you can realistically achieve.

You can achieve that trivially by exiting if something goes wrong when calling the API. And where exiting is not possible because it's a longer running application, concerns have to be separated: If there are multiple pieces of code that need the same data, feed those pieces the data they need from a central location that interfaces with the API. And have the error handling logic (which is usually higher level control logic) in the central location.

Have you considered using patterns that help dealing with that?

like Railway Oriented Programming

https://www.youtube.com/watch?v=45yk2nuRjj8

I believe that almost every thing should return Result<T>, because almost everything can fail and compiler should scream when you do not handle those fail pathes.

That makes me believe that C#'s "FirstOrDefault" for value types sucks, because you're never sure whether the "Default" comes due to lack of value or because the found value is actually the same as default

e.g

var list = new List<int> {1,2,3};

var found = list.FirstOrDefault(x => x < 1);

found = 0 (default)

meanwhile 0 may be valid value! so we aren't sure whether it is error or an actual value

and we have to perform e.g casts to `int?` or stuff to detect that.

using Result<T> gives very precise information

> meanwhile 0 may be valid value! so we aren't sure whether it is error or an actual value

This function is useful when you don't really care whether it's an error or the value. Imagine you're querying the view count of an item of the user. If the user is anonymous, the select might give an empty result, but you only care about showing a number to the user. So 0 is absolutely fine in that case.

If it's important to you whether the item actually exist, you're using the method in the wrong place.

Yes, I can always use First and catch Exception, which is meh.

First returning Result<T> or something like FirstOrNull would be better, because FoN would work for ref types - classes and stuff the same way (afaik) as FoD does, but it'd make error handling for value types like ints more precise

This was bread and butter error handling in COM, everything old is new again!
Yep, all hail the mighty

    #define CHECK(com_expr) hr = (com_expr); if (FAILED(hr)) { return hr; }
Ah, the memories... glad I don't have to touch it ever again. "And a million other things that, basically, only Don Box ever understood, and even Don Box can’t bear to look at them any more".
COM is where all new OS APIs land nowadays, since Vista.

Even if they are eventually wrapped in .NET libraries.

Not sure why people don't like exceptions. Throw different error classes according to the source of the problem and just handle differently in the upper classes.

throw new ErrorUser('Bad input')

-> Show friendly error messages.

throw new ErrorFatal('Db unavailable')

-> Email error to dev and quit.

I never like the verbosity of returning errors from each methods.

How hard is it to trace the stack when you're supposed to be using error logging tools like Sentry?

It's a trade I'm willing to take for simplifying reasoning about non-happy-paths. Try catch is great in theory but it's super easy to shoot yourself in the foot with in bigger projects, either because it's non exhaustive or someone took the easy way out and made a catch which isn't fine grained anough.