Did you really find the example imperative error handling code more elegant than the railway-style function composition?
Did you really find the example imperative error handling code more elegant than the railway-style function composition?
1) Good luck annotating all potential cases of divide by zero or IO error, and all callers of those.
2) Everyone must pay the runtime cost of wrapping and unwrapping on every operation, even if nothing fails.
3) The type of a function that can fail in ways A and B isn't the same as the type of a function that can fail in ways B and A.
4) Good luck getting a stack trace.
I think unchecked exceptions are better for most applications, where errors are rare and have many different types.
That's a great point. At the end of the day, Either and (checked) exceptions are equivalent from a semantic point of view, but having exceptions being an explicit part of the language means much better support for tooling and debugging.
Exceptions are not too bad, but they are very often misused.
This! The worst is when people start using them to control the flow of business logic. I hate that.
I'm a data scientist, not the software engineer, so forgive me if this is something you all learned in CS 101.
Also, constructing an exception object, with the full stacktrace etc., is a pretty heavy operation and can cause big performance issues. But it's mostly the first thing.
* Which is an important distinction. Flow control exception are recognisable because you intentionally throw an exception and expect it to be caught by a particular catcher to process it in a particular way - much like a GOTO, and with all its fragility. In the proper usage of exceptions, when you throw an exception you don't care which line of code catches it and when you catch an exception you don't care which line of code threw it.
Validation cases like the examples on the article are not exceptional and the failure cases must be handled properly. In this case it makes sense to use an Either -like solution where the compiler will warn if there are unhandled cases.
Both styles of error handling are useful in some cases and both have their issues.
As for 2) the exceptions are not zero-costs even with modern implementations and profile-driven optimizing compiler makes the runtime cost of wrapping/unwrapping low.
For point 1, if you want to use the railway approach instead of exceptions, you must annotate all functions in the chain.
For point 2, I was talking about the runtime cost.
For point 3, Result<Result<Foo,A>,B> isn't the same type as Result<Result<Foo,B>,A>. People in the Haskell world acknowledge that it's a big problem ("monads don't compose"), hence all the research on monad transformers and extensible effects etc.
For point 4, if your function is the lowest in the call stack and decides to report an error using the railway approach, you don't get a stack trace.
Foo -> Result<Foo1,Error>
Foo1 -> Result<Foo2,Error>
Foo2 -> Result<Foo3,Error>
And use bind to compose them easily.I think the confusing part is the word "exception" as it mixes concepts.