4 ms·
Another aspect that I think is often not well understood when you get this "standard Monad error handling tutorial" is that the monadic approach is nicer precis
by jerf 4y ago
Another aspect that I think is often not well understood when you get this "standard Monad error handling tutorial" is that the monadic approach is nicer precisely because you're not handling errors. The reason why the monadic code always looks cleaner isn't only that it's "monadic" but also because the "standard Monad error handling tutorial" deliberately doesn't handle any resulting errors. Monadic code like (sorry, I know Haskell syntax, not F#)
user <- getUser
message <- getMessage dbConn msgIdx
renderMsg user message
looks so much cleaner, but if you actually need to unpack the result of getUser to figure out what the error is in order to do something specific based on an authentication error versus a database connectivity error versus a timeout error, the syntactic advantage over Go dissolves. I forget the exact Haskell syntax now but think something like
user <- case getUser of
Error err ->
case err of DBerror e -> ...
AuthError e -> ...
Timeout -> ...
default -> ...
Result u -> return u
message <- case getMessage dbConn msgIdx of
Error err -> ...
and so on, etc. Yes, it's great that the happy path looks that nice but monadic error handling ends up affording the same problems that lead people to complain about exception based handling. Most of my Go code would only be marginally improved by "better syntax", the bulk of it is the actual logic of handling the many and manifold problems that arise in network servers, which is most of what I use Go for.
Syntax is almost never my blocker. Can't quite say in good conscience "never". But it's not my biggest problem. My biggest problems are generally that I didn't realize that API could return a response like that, or what do you mean TLS is rejecting my certificate, or most of all, "Oh, guess I didn't understand the problem as well as I thought".
- tsimionescu 4y agoI think it's actually worse than exception-based error handling, as exceptions are typically integrated in the runtime in such a way that they produce a nice stack trace, which is very very often all the context you actually need (though sometimes parameter/variable values are important as well). The Result/Either/Error/[...] types have no such special support, so they often indeed just give you a single error type and no other context. On the other hand, it's important to note that, except for maybe adding context and bubbling up, the vast majority of code in any code base will not actually handle errors, so the advantage of taking away the huge amounts of boilerplate when possible are still worth in my experience (as someone who has ~7 years of Java experience and have moved to Go for 3 years, all in a commercial setting with a large-ish team).
- valenterry 4y agoWhat does make people always think that the two things don't go together? In Scala I use Exceptions, but I don't throw them, I use monadic composition style. So I end up with clean code AND a stacktrace, just as if I had thrown them. Sometimes I wrap them for even easier debugging, e.g. for { user <- getUser message <- getMessage } yield showMessage(message, user) And then when I call this function I do renderMessage(x).mapError(e => RenderMessageError(message = x, cause = e)) Which, in case of an error, gives me a typical jvm stacktrace Exception in thread "main" com.myproject.module.MyProjectFooBarException: Message x could not be rendered at com.myproject.module.MyProject.anotherMethod(MyProject.java:19) at com.myproject.module.MyProject.someMethod(MyProject.java:12) at com.myproject.module.MyProject.main(MyProject.java:8) Caused by: DatabaseError: User for ID ... not found at org...(Fraction.java:143) at com....(MyProject.java:17) ... 2 more
- tsimionescu 4y agoI'm not familiar with Scala or its integration with the JVM, but in Java at least, a stack trace is only collected for an exception from the moment it is thrown. If you create an exception object in one function, return it as a simple value, and then throw it from a new place, the stack trace will not show when the exception was created, only when it was thrown. This would mean that, in your example, if getUser were returning an error, the stack trace wouldn't show that, it would only point to the mapError() function. Is this correct, or does Scala actually integrate with the JVM such that it can show the original source of the error?
- valenterry 4y agoI don't think what you say is correct. See: https://stackoverflow.com/questions/69065097/why-java-stack-traces-are-generated-when-exceptions-are-constructed https://stackoverflow.com/questions/69065097/why-java-stack-... Hence it works in Scala (and also Java) without throwing Edit: maybe what you meant to say is that the stack is only unwinded at the time of generating the stacktrace and not when the exception is created? Which is true and a good thing since it's expensive to create it.
- larve 4y agoexactly, the monad "erases" the error, at least in the "happy path", while go makes it first citizen. one is not better than the other. I care a lot about errors, for logging, for metrics (how many connection errors did people from canada have last month?), for custom error handling (UX), for lower stack depth, etc... With tools like github copilot, I can now write extensive error handling code at the speed of my tab-key, which is a game-changer. But if your team likes monads and doesn't care about errors (or you have other monads you can now lift into this with a single line of haskell), or you want to have a nice functional separation for easier unit testing and less coupled codebase, then the monadic approach makes more sense. But that choice is one you can make even if you think in terms of monads when doing error handling.
- deleted 4y ago[deleted]
- zozbot234 4y agoYou don't have to use pattern matching syntax. You can just compose your get* functions with a custom function that adds whatever context to the error case. It can be made quite clean.
- xupybd 4y agoIt's about separation of concerns. The happy path is one flow, and the error path is another. The business logic is no longer mixed with the implementation details. You can look at the code and very quickly reason about the business logic. Yes the implementation details still exist but they can be reasoned about on their own.