5 ms·
The problem with this is that _you've lost all the wrap messages_! In a good golang codebase, you get human-readable stack-trace with the relevant variables pr
by lalaithion 4y ago
The problem with this is that _you've lost all the wrap messages_!
In a good golang codebase, you get human-readable stack-trace with the relevant variables printed inline on every error. It's super useful and every "Result" based error handling implementation totally whiffs on this.
But it doesn't have to. You _can_ get the best of both worlds if you take a moment to appreciate the benefits of the golang style and provide simple to use APIs to build context for every possible error.
- klodolph 4y agoYes, the messages! In Haskell, you would get the effect with a higher-level function that can add context, or something like <?> in Parsec, or maybe a custom function that calls catchError. In the continuation-passing style like you get in JS promises, you can do this with .catch(err=>...).
- fredrikholm 4y agoI was thinking the same thing. The errors are now returned without their context, and I've written enough F# to know that computational expressions aren't magic. Forgetting an exclamation mark circumvents the entire expression. Perhaps that's changed in recent years, but it was really annoying to find your code silently breaking due to a missing symbol some ~5 years ago. As much as people love to hate on Go, it's the only language where I can read someone else's code line by line and not simultaneously have to read partly outdated language specs, no longer applicable SO answers or generally have to play internet detective to unravel magical incantations that sometimes fail at runtime. Yes Go is verbose, yes it lacks XYZ. Junior, mid, senior, Go newb, Go expert, the code (for the most part) looks the same. How that's a bad thing I don't get. Boring? Incredibly. Useful? Incredibly.
- xupybd 4y agoI like using the bind operator and avoiding computational expressions. Like Scott Wlaschin suggest. But I've also found that sometimes traditional error handling makes more sense. https://fsharpforfunandprofit.com/rop/ https://fsharpforfunandprofit.com/rop/
- Arnavion 4y agoHow would you manage to forget a `!` when writing F#? IIRC the code wouldn't type-check without it, because you'd be trying to use an expr of the monad type as if it was of the inner type. It's been a few years since I wrote F# so I could be misremembering.
- fredrikholm 4y agoThis was in the context of async, where bind meant awaiting (or not, can't recall). The outcome was unpredictable async behaviour. To make things worse, back then the async story wasn't all that great to begin with. That was on top of the already confusing "will this run in parallel"-bingo situation of .NET. This was before the .CORE-1/2/3/NET-without-the-dot period, no idea if it's any better now. Hiding complexity in a very expressive language (F# is beautiful), running on top of a complex VM that is constantly changing... in the context of working with tight deadlines and other peoples code, I don't mind if statements. Happy to hear that the compiler catches other (most?) cases. Perhaps I should give F# another round, it's mostly .NET that I can't find myself to relearn for the nth time.
- Arnavion 4y ago>where bind meant awaiting (or not, can't recall) Yes, but that's my point. If you have an `Async<int>` and you forget to use `!`, you'll get a type error if you try to use it as an `int`. Even with an `Async<unit>` where you think you're executing some side effect but forget to use the `!` to execute that side affect, the `Async<unit>` value will be unused and you'll get an error about that.
- fredrikholm 4y agoYou're probably right; I don't remember the details. What I do remember was parallelism being dodgy, several team members looking at it until one added an exclamation mark and solved the problem. This was F#3 in a legacy project, some five years ago. I bet lot of things have changed since.
- erik_seaberg 4y ago“Code looks the same” means your experts will forever be forced to wade through the same awful boilerplate as your beginners. There’s no payoff for learning more, and no way to improve their work (apart from code generation, which I strongly endorse here).
- fredrikholm 4y agoAnd? I'm not trying to be facetious here. I genuinely don't understand the problem. > no way to improve their work What do you mean? If you mean abstractions, there's functions, methods, interfaces, types, packages, generics etc. What exactly is missing, and how much more productive would everyone be if it was present?
- jerf 4y agoAnother 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).
- taberiand 4y agoI might be missing something. Wouldn't the C# solution to keep the wrap messages be to add a MapError method? Something like: public ErrorChecked<T, E> MapError(Func<E, E> op) => _value is not null ? Value(_value) : Error(op(_error!)); Used like var result = from thing1 in DoFirstThing().MapError(ex => new Exception("Failed doing first thing", ex)) from thing2 in DoSecondThing(thing1).MapError(ex => new Exception("Failed doing second thing", ex)) select thing2; Which, maybe it's just me, but I think that looks cleaner than the Go code.
- lalaithion 4y agoYes, that is the solution; I'm complaining that it wasn't even mentioned as a possibility in the blog post.
- taberiand 4y agoOh right, fair enough. I usually end up doing the Go style in C# anyway; the from ... syntax usually leaves other devs unconvinced and suspicious.
- lalaithion 4y agoHaskell could have (I don't know a library that implements this, but it's ~trivial) do x <- open filename `errorContext` ("failed to open " <> filename) y <- readBytes x `errorContext` "failed to read bytes from file" Rust has let x = open(filename).with_context(|| format!("failed to open {}", filename)?; let y = read_bytes(x).with_context(|| format!("failed to read bytes from file")?; or if we ever get https://github.com/joshtriplett/rfcs/blob/simple-postfix-macros/text/2442-simple-postfix-macros.md https://github.com/joshtriplett/rfcs/blob/simple-postfix-mac..., let x = open(filename).with_context!("failed to open {}", filename)?; let y = read_bytes(x).with_context!("failed to read bytes from file")?;