8 ms·
I like Go's error handling. Yes, I know that puts me in a special category of coder. Reasons: - It's simple and easy to understand. There's no hidden complexi
by marcus_holmes 3y ago
I like Go's error handling. Yes, I know that puts me in a special category of coder.
Reasons:
- It's simple and easy to understand. There's no hidden complexity, no magic, no gotchas. Nothing is going to surprise me about it.
- It's a very readable cadence. Do the thing, check the error, do the thing, check the error, do the thing, check the error. Once you get used to the error checks it's extremely readable.
- Despite all the extra characters the actual mental load of the error checks is tiny. Yes it's more typing, but the hard bit about code is thinking not typing, and it doesn't make more thinking.
- If I have to do something special with the error, it's easy. There's no incentive to handle this error just like the rest, or ignore it and let the exception handler catch it. If this error needs (for example) extra logging then I can just put it in there for this error check, no hassle.
- It's pretty much standard across all Go code. If I have to deal with someone else's code, I can expect the same cadence, the same simplicity, the same readable pattern of error checks. One of the great things about Go is that it is opinionated about stuff like this.
- lmm 3y ago> - Despite all the extra characters the actual mental load of the error checks is tiny. Yes it's more typing, but the hard bit about code is thinking not typing, and it doesn't make more thinking. The hard part is understanding the existing code. The more cluttered and verbose the code is, the harder that is. Go's boilerplate if err != nil return err, nil becomes something that your eyes just skim over - which is fine right up until you have some code that's doing something similar but not the same, and don't even notice.
- marcus_holmes 3y agoI find that the differences leap out at me. Even just a `err == nil` instead of `err!= nil` is noticeable. I do have to spend a second reading the action if it's not just `return result, fmt.Errorf("failed to do the thing: %w", err)` but that's good, I think. And all of this is way easier than trying to trace up through the stack to the nearest exception handler and work out what it will do with the error edit: also, verbosity doesn't make code harder to understand, imho. If anything the other way around. Packing 5 statements into a single line is massively harder to read than separating those same 5 statements into 20 lines with error handlers.
- lmm 3y ago> And all of this is way easier than trying to trace up through the stack to the nearest exception handler and work out what it will do with the error You still have to do that part though? Like, this function returns err, so the caller returns err, so the caller of that returns err, ... - you've still got to walk up the stack to the point where the error is actually dealt with. > edit: also, verbosity doesn't make code harder to understand, imho. If anything the other way around. Packing 5 statements into a single line is massively harder to read than separating those same 5 statements into 20 lines with error handlers. Very much not my experience. There's a huge understandability hit when a function doesn't fit on a single screen and you have to scroll, so vertical space is really precious.
- marcus_holmes 3y ago> you've still got to walk up the stack to the point where the error is actually dealt with. This is why we wrap errors. The error message gives a pretty good indication of what the stack was doing when it went wrong. I had a junior dev work with me on some JS. I was writing it in functional style because it made sense at the time. He was really struggling, so I refactored it to old-school imperative and he understood it and was able to work with it. It might have been an issue with the way he was taught JS, but I think it's more that tightly-packed concise code is actually harder to parse. Not least because you have to understand the whole thing to work out wtf it's doing. Whereas with one-statement-per-line you can scan down to the lines you're interested in and focus on those.
- kitd 3y agoGo's boilerplate if err != nil return err, nil becomes something that your eyes just skim over OTOH, if you make sure you do something like if err != nil { return fmt.Errorf("what I was doing when the error happened: %v", err) } then you get very precise targeted errors appearing in logs that are much easier to track down and fix than either just returning the error or the usual generic catch block found in other languages. IME properly handled Go errors make for much more maintainable code.
- kaba0 3y agoMaybe even include a stacktrace, and you got what any sane language with exceptions had for decades, hurray again for Go!
- kitd 3y agoThe problem (as alluded to above) is that stacktraces give you no context to what went wrong, only where it happened and the (possibly cryptic) underlying error. IMO the real value in proper error handling (ie what makes fixing errors easy) is that context, not the precise line # of the error which, though useful, is supplementary data. Rust errors with anyhow::Contexts are far more workable than those without. Edit: I actually wrote a simple Result handling package for Go that includes error context and stacktraces: https://github.com/kitd/chock https://github.com/kitd/chock
- kaba0 3y agoThat’s why you throw an exception, catch it some level up, and rethrow it wrapped in another exception, with proper context.
- kitd 3y agoI think we're arriving at the same solution, with approximately the same amount of boilerplate if done to same degree of accuracy.
- marcus_holmes 3y ago
- threeseed 3y agoThe problem is that it's so incredibly inflexible and dated. Many effectful actions e.g. reading from a file system can have a range of different errors each of which you want to handle differently e.g. out of disk space versus lack of permissions. It's great that you're treating errors as values. But you need pattern matching and other techniques as they make your code more: (a) readable, (b) safer, (c) simpler and (d) less verbose. The hilarious thing is that eventually Go is going to get these because there is nothing but upside. And then at that point you're going to wonder how you ever survived without it.
- Kamq 3y ago> But you need pattern matching and other techniques... The go way to do this would be: switch { case errors.Is(err, outOfDiskSpaceError): // handle out of disk space error case errors.Is(err, lackOfPermissionsError): // handle lack of permissions ... default: // do the equivalent of the `_` case in a scala match statement or `t` in a lisp cond } Which is roughly as readable as scala/rust's match statements. Moreso if someone tried to get cute in scala and bind both the object as a whole and parts of it at the same time ( something like `case x @ Type(_, _, y, _)` ) or when people get real cute with the unapply method. I mean, I like scala. It's fun. But I would never say it's more readable than go. I've been left to support things that happen when a team of average intelligence devs get a hold of it. You're also really comparing an MIT and New Jersey style solution here, and judging them both on MIT merits. And I don't think that's exactly a fair argument.
- blueberry87 3y agoTrying to compare a switch statement to proper pattern matching is like comparing a rock to a rocket.
- di4na 3y agoI am unashamedly stealing this one, it is both apt and making me smile. Kudos
- 3y ago
- dd82 3y ago- Simple and lots of duplicated code. Golang took DRY overuse and turned it on its head to be dupliate everthing everywhere - Its duplicated everywhere which means I need to mentally figure whether I need to figure out this path or ignore. Mental load of typing is small, load of reading is not Golang has alot of good things about it. This is not one of them and is a wart on the language that is tolerated because the genesis of the language is to be an entirely inverted approach to verbosity than Java. Its not something to be praised.
- marcus_holmes 3y agoThe mental load of reading a lot of simple code is less than reading a small amount of complex code, I find. It's easy to look at a 50-line function with one statement every 5 lines and find the bit I'm interested in, because it's easier to screen out the bits I'm not interested in. Rather than unentangling a 5-line function that has 10 statements in it, because I have to work out what all of it does in order to understand it and I can't focus in on the bit I'm interested in.
- et1337 3y agoThe problem with lots of simple code is now there’s 5,000 slightly different ways of doing the same thing. You end up trying to wrangle it with a mess of code generation and linters.
- marcus_holmes 3y agoYeah, this is something I think feels like a bigger problem than it actually is. If I change the way I'm handling something, it is a lot of typing. But it's just typing. No big deal.
- setr 3y agoThe usual issue isn’t the typing, but the fact that you need to make sure you identified and updated every site correctly. DRY isn’t about reducing LoC
- kaba0 3y ago> It's simple and easy to understand. There's no hidden complexity, no magic, no gotchas. Nothing is going to surprise me about it There absolutely are gotchas, exactly because they are not sum types. There are functions where both “slots” are used as return types, when an error occurred. > It's a very readable cadence. Do the thing, check the error, do the thing Arguably, you can’t reasonably handle most errors in-place, you just don’t have enough context for that. Also, you want to make your business logic right — all those verbose, often incorrect/naive error handles will just make it harder to read your own logic. Also, very easy to accidentally swallow an error - exceptions/sum types are much better in this regard, you can’t not care about them.