Exceptions end up being an elegant way to represent this, you can choose how much you handle and where and match against the types of errors. Everything else I have tried has been worse, forcing me to handle errors I don't care about at that point in the code with a boilerplate response to send it upwards, something exceptions do automatically. I don't get more robust code with explicit error handling I get more verbose code for errors I can't do anything about. In practice it also changes the interface of my methods far more with maintenance and propagates error handling code throughout.
What I found really, really nice for error handling is dataflow-style programming, because the main flow only has to deal with the happy path. If you don't have a result, you simply don't send anything to the next filter in the pipeline.
And you report errors using a stderr style mechanism, so error handling can be centralised and at a high-level, where you actually know what to do with the errors.
Try not to bubble up errors. Many subsystem interactions can be unidirectional. If an error occurs, it is stored right there. The caller might not even care. The situation can be handled later.
Imagine a situation as simple as:
foo = ComputeSomething()
Where ComputeSomething() is a complex operation, spread into multiple functions, with many points that can fail for various reasons. If a function four layers down the stack from ComputeSomething() reports an error, this means all three functions above it need to also pass it back as Result<T>, and if you try to avoid the if-else hell, it means the entire call graph under ComputeSomething() will need to be wired with a monadic interface that smartly doesn't execute functions when Result<T> is of Error type. All it does is make you zoom through the call graph doing nothing, just to simulate the default behavior of an exception bubbling up the call stack.The most obvious idea is: instead of retrieving something 4 layers down, get the information first at the top level, and if there was no error push it down.
I've stopped thinking that syntactic features can solve structural problems. That applies to algebraic types just as it applies to exceptions.
In more complex systems, this won't work out. Imagine a software that controls sensors and motors, for example. What if one of these devices fails? There is no way any automatic system (like exceptions) could come up with the right way to handle this situation.
In this way, the client code is basically required to think -- oh, wait, what happens if I'm not getting updates for some time beyond what is acceptable for me? Also, in this way, the situation can be handled in a single central location.
That's suitable for batch programs and other short running things where you can just scrap the incomplete computation and re-try with a blank slate. I'm sure that can be productive for webapp code that sits between a database and the frontend.
For longer running processes and more complex systems, exceptions are not so great. It's almost a given that you will have a hard time tracking down all the error scenarios in your system.
Because the stack unwinding cleanup is the same when an exception occurs or when an operation completes normally this app could recover from anything.
> It's almost a given that you will have a hard time tracking down all the error scenarios in your system.
You don't need to track down all the error scenarios. The only thing you need to worry about is things that you can recover from to retry and things you can't.
Passing errors around places too much emphasis on where errors occur. For recovery you only need to know what you can recover from and where you can do that recovery. This is usually no where near where the error occurs.
This is actually the best argument why exceptions do not "scale": Because in larger systems, the place where you handle the error is almost certainly not up the call stack, but in a different subsystem.
> Passing errors around places too much emphasis on where errors occur. For recovery you only need to know what you can recover from and where you can do that recovery. This is usually no where near where the error occurs.
In a way, errors are just data, like everything else. There is not much sense in making them something special - as I said, with the exception of smaller script-like programs, where you usually want to jump out of a larger subprogram immediately, and rely on the garbage collector / stack unwinding to clean up (hopefully, everything) for you.
The reason you treat them as special is because at the point an error/exception occurs you are saying that your function (or program) can no longer do any more meaningful work, forward progress is now impossible, and you can't return anything meaningful.
We've gone backwards in making error returns explicit because the most common and intelligent thing to do is pass the error back up the chain or right out of the subsystem entirely. The lazy approach is the correct one with exceptions.
> Because in larger systems, the place where you handle the error is almost certainly not up the call stack, but in a different subsystem.
If your subsystem is network connected then the most likely cause of an re-startable operation is a minor network issue. There's no need to inform another subsystem immediately. I don't really see how it's an argument that exceptions don't scale. If you need to inform another subsystem you can do that.
I disagree. I absolutely love throwing exceptions from lower levels of my web apps and letting them bubble to be handled by exception mappers that will formulate the correct http response from them.
Many "problems" that people have with functional programming occur because they are trying to use FP techniques in a language that isn't really equipped for functional programming (JavaScript, C#, Java, Go...).