3 ms·
Having used Go a lot recently the err!=nil boilerplate is only marginally worse than the monad approach which ends up with a lot of match statements. Where mona
by PaulKeeble 6y ago
Having used Go a lot recently the err!=nil boilerplate is only marginally worse than the monad approach which ends up with a lot of match statements. Where monads work better is chaining calls together, but at the cost of the of choice how to handle the errors.
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.
- jstimpfle 6y agoThe key is to collect the errors in the subsystem where they originated and to handle them at an appropriate time. 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.
- ivan_gammel 6y agoAppropriate time is quite often at the end of the unit of work. Let’s say, your microservice is doing some interactions with database and querying another services. If an unlikely I/O error happens, DB will take care of the uncommitted transaction and your service is just fine with returning HTTP 5xx. Probability of this happening and impact of not implementing error handlers all over your stack are very low. Since the majority of developers are not writing perfect programs, they will not spend the money/time to avoid this risk. Exceptions may not help writing a better program, but they are practical enough to be supported in the language.
- jstimpfle 6y agoThis absolutely works for a HTTP request handler, where all the error handling is implemented in the DB / garbage collector, and where we're dealing with almost perfectly isolated processes. 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.
- ivan_gammel 6y agoThere's no silver bullet that would solve error handling in such cases purely by the means of the programming language. Just like it's possible to have memory leaks in Java, it is possible to write broken code with Either<Result, Error> - imagine accidentally ignoring an error from sensor that results in overheating and fire. If you want to absolutely make sure, that this won't happen, then you build your system in a way, where prevention of the exceptional situations is implied by design, rather than requires an additional effort from developer.
- jstimpfle 6y agoYup. To continue with this example, an approach that often works is to simply not have any error. For sensor readouts, request new samples to be reported asynchronously. (Coincidentally this is better for performance as well). 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.
- TeMPOraL 6y agoSubsystem is too big of a boundary. GP and GGP are likely talking about function level - which is where I also experience this. 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.
- jstimpfle 6y agoDo you have an actual example where you do something 4 layers down the call stack from a subsystem entry point and (only) the lowest level can fail, and furthermore fail in a way that all the layers up the call chain need to bubble up the error? Maybe that can be rewritten so the error occurs / is handled at a more suitable place? 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.
- mpweiher 6y agoYeah, the problem is call/return: you have to return something. If you have nothing to return (or not yet → async), things get tricky. 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.