Hacker News new | ask | show | jobs
by throwaway894345 1516 days ago
The latter is a much stronger argument than the former (no idea why people get so worked up about character counts), but even then, "shit" is really strong considering how often one experiences exception traces when using an application written in Python or Java or some other exception-based language. Point being, we should probably evaluate error handling schemes based on results rather than ideology (even though I tend to agree with some of that ideology).
4 comments

Screen real estate is limited, especially vertical real estate. Compared to languages with saner error handling, I can read approximately 25% as much Go code at once. That's a real cognitive burden when maintaining code or learning your way around a new codebase, which seems especially egregious from a language whose community consistently proselytizes about how the lack of language features is great for maintainability and onboarding.

I'd take exceptions any day of the week over Go's solution. I'd much rather the program crash by default than attempt to continue with corrupt data by default. I'd rather have concise, explicit, compiler-required error handling than exceptions, though.

I don't think "screen real estate" is the right argument here.

The problem is just that every line creates cognitive load and there's a tradeoff between concision and descriptiveness.

A language with piles of syntactic sugar and magic gets it wrong with too much concision and can read like line noise when it gets overused.

Go goes the other way though and makes it way too verbose and just makes it difficult to read the code. When a method needs to have 6 different error handling clauses in it, then it isn't as clear that 5 of those just bubble up the error while one of them has some unique handling. It also increases the chances that some programmer copypastas the boilerplate bubble-up code to all six of those cases and it sails through PR review. You can write a static analysis linter to force programmers to always handle errors and not ignore them, but you can't force them to handle errors correctly. When humans are reading the code, concision helps and verbosity hurts -- up until that crossover point where magic causes readability to suffer.

Go programmers seem to focus on abhoring magic and rejecting the benefits of concision. But when it comes to PR review your job is to stare at the whole method (or the whole file) and be able to "see" the bug, and more lines of code will make this job more difficult (and is also why some of the recommendations of the "clean code" book are pretty bad since extracting more tiny little methods can harm overall readability). There's a happy optimum somewhere where cognitive load is minimized. That isn't attained though by just having the simplest language design possible and offloading complexity into more verbose code.

I disagree. IMO, there's much more cognitive load in parsing dense, "minified" code than there is in scanning code whose control flow mirrors its visual structure. Humans are very good at seeing visual structure (which is why we tend to indent, split code across lines, and other syntactically insignificant usage of whitespace). By convention in most mainstream programming languages, this visual structure mirrors code flow, so we can see the control flow at a glance; however, many languages have special hidden control flow (exceptions) or control flow which is otherwise isn't part of the visual structure and thus easily overlooked at a glance (e.g., Rust's `?`). In my opinion, this "hidden" control flow allows more errors to slip past reviewers (though some languages might recoup some quality by other means).
So, the thing specifically about ? in a language with Result is that you can read some code that uses it and not worry about what happens for Error cases if that's not currently your focus - the question marks aren't a "Look at me!" focus the way something like try-catch is.

But if you are wondering about Error cases, they are there to see when you're looking for them because that ? while unobtrusive is something you can look for.

I'm sure in most IDEs you could have it highlight ? in a "Looking for error handling" mode if that's what you want.

Note that Rust does not consider control flow to be something the core language owns exclusively, you can return core::ops::ControlFlow to say actually I also have an opinion about whether you should keep going, this can make sense for a closure or function intended to be called inside an iterator or other loop context. Some of the ergonomics for this aren't finished, but what is there is already useful where a Result would work but is ugly because your early exit scenario isn't in fact an error at all.

> not worry about what happens for Error cases if that's not currently your focus - the question marks aren't a "Look at me!" focus the way something like try-catch is

Error handling is no less important than the happy-path.

I mean I spent quite a few words talking about how there's a happy optimum where beyond that you start to get too much magic and code gets too terse and unreadable.

You just did prove my point though which is that this is the only argument that Go programmers consider, and they blindly reject that adding more lines of code can harm readability.

> You just did prove my point though which is that this is the only argument that Go programmers consider, and they blindly reject that adding more lines of code can harm readability.

Can we lower the rhetorical temperature a notch? Just because someone disagrees with you doesn't mean they're "blindly rejecting" your reasoning. In particular, I'm not just a Go programmer--I've used Java, C#, Python, JS, C++, and C in various professional over the course of my career and I've also played around with dozens of other languages and I have more experience with several of those languages than I have with Go. My opinions are shaped by those other languages at least as much as they're shaped by Go, and indeed I didn't start out having these "pro-Go" opinions--rather, I adopted them over time after allowing my preconceptions to be challenged. Note also that some of my preconceptions haven't changed--I still think sum types and enforced handling of return values are a good idea, for example.

I was never arguing for "minified" code, which is ridiculous. Lower your own rhetorical temperature.
I don't think not handling errors after every single method call, makes the code dense. Its just way easier to read. 99% of the time you're just going to wrap the error in your own error and return so why not just have a single place that does that?
Cognitive load is unrelated to SLoC.

This expression

    let a = x.iter().filter(...).apply(...).map(...);
is equally or even potentially _more_ cognitively complex than this expression

    for _, v := range x {
        if !filter(v) {
            continue
        }

        vv := apply(v, ...)
        vm := map(vv, ...)
        ...
    }
focusing on a pedantic detail that i clearly didn't intend and which doesn't change my point.

consider it from a blocks-of-code metric, or some better slightly more abstract metric, that isn't affect by simple things like whitespace transformations, and try assuming that we all understand that we should write code that isn't monstrous to begin with.

"Monstrous" is an opinion, not a metric.

I would personally much rather maintain the code in the second example than in the first.

> Screen real estate is limited, especially vertical real estate. Compared to languages with saner error handling, I can read approximately 25% as much Go code at once.

In my experience, people can't actually read everything on the screen at one time anyway, and the more dense/terse things are the harder it is to read (otherwise we would minify everything).

> I'd much rather the program crash by default than attempt to continue with corrupt data by default.

It's not likely that it will continue with corrupt data because you can't use the return value without explicitly ignoring the error. It's not perfect, because there are cases where you want to crash when there is an error but no return value, and Go doesn't help you here. I would like to see this improve, but it's relatively low on my list of qualms with Go (I would rather have sum types, for example). It certainly isn't worth changing languages over especially since, in practice, Go seems to have fewer error handling bugs than exception-based languages.

> In my experience, people can't actually read everything on the screen at one time anyway, and the more dense/terse things are the harder it is to read (otherwise we would minify everything).

Whether or not you can read everything on the screen at one time is missing the point entirely. The point is that context matters, and the more frequently you have to scroll to find it is more cognitive burden.

> It's not likely that it will continue with corrupt data because you can't use the return value without explicitly ignoring the error.

It is far too easy to accidentally do the wrong thing with an error in Go. In Rust, for example, no matter what you want to do with the result of a fallible call, you have to do it explicitly. If you want to crash on error, you `.unwrap()`; if you want to bubble it up, you `?`; if you want to continue with a default value, you `.unwrap_or()` or one of its variants.

> in practice, Go seems to have fewer error handling bugs than exception-based languages

This is based on?

> Whether or not you can read everything on the screen at one time is missing the point entirely. The point is that context matters, and the more frequently you have to scroll to find it is more cognitive burden.

And I disagree. Scrolling IMO is a lot easier than squinting to parse dense code. We have visual structure (indentation blocks and so on) for a reason. The visual structure aids in readability, and indentation blocks help the eye scan quickly over a document. The visual structure in most languages resembles control flow, except some languages make an exception (no pun intended) to this rule for error handling paths which are not easy to see at a glance.

> This is based on?

My experience.

> Scrolling IMO is a lot easier than squinting to parse dense code.

This is a false dichotomy - there's a third option, which is not squinting (because, presumably, you're doing so because you decreased your font size), and being able to see more on the screen at the same time.

Moreover, scrolling is bad for cognition. It's pretty well-known that the human brain likes to use spatial maps - that's the reason why memory palaces are so effective. Scrolling decreases the ability of the brain to make spatial maps compared to, well, not scrolling.

> The point is that context matters, and the more frequently you have to scroll to find it is more cognitive burden.

This is not something you can "disagree" on - divorcing information from context always leads to more cognitive burden.

> This is a false dichotomy - there's a third option, which is not squinting (because, presumably, you're doing so because you decreased your font size), and being able to see more on the screen at the same time.

It's not a false dichotomy. Visual structure (via whitespace) comes at the expense of strict information density (assuming a fixed font size). If this is not true, then we would never have any (syntactically insignificant) whitespace.

> This is not something you can "disagree" on - divorcing information from context always leads to more cognitive burden.

Agreed, but this supports my point. It's a lot easier to scroll and scan visual structure than it is to reparse dense code. Density divorces us from context a lot more than physical distance on a screen.

How is that different than an early return? Exceptions basically reuse the existing stack-oriented structure of programs - it will do the same thing as if you would have returned from that point, unless you use a try-catch block, which again guides the eyes very well. Compared to that repeating the same repeating pattern will just introduce useless noise that will make identifying the actually important greater pattern (e.g. manual bubbling up) harder to see.
> > in practice, Go seems to have fewer error handling bugs than exception-based languages

>

> This is based on?

By explicitly annotating functions as fallible the language hints to the programmer that errors need to be accounted for.

With exceptions, the hints only appear at runtime - when your program crashes. There's nothing that nudges you towards handling errors at the point of writing code, so you end up with brittle software.

Checked exceptions are a thing. Java’s implementation is unfortunately not perfect, but exceptions themselves are analogous to basically Rust’s Result type, but it has in-built support on a language level which packs the stack trace into the error case and auto-bubbles up if not handled.

I believe a language where instead of subtypes you would get algebraic data types and could optionally mark whether a given exception is checked or not would be the ideal solution.

> auto-bubbles up if not handled

I'm guessing you already know this, but for anybody else reading - this isn't entirely accurate; Result::Err doesn't auto bubble up like an exception, you have to manually bubble it up. The "special sauce" comes from (A) the compiler forcing you to notice this and do something about it, and (B) the `?` syntactic sugar to make that super easy.

It does occur to me as I'm typing this that you might be talking about panics, though, in which case yeah that's entirely accurate.

Additionally, the more repetitive code there is, the more opportunities there are for some subtle difference to be lurking in one particular chunk. And with pervasive boilerplate it becomes easier to eyeglaze past that subtle difference. Whether that difference is a bug or intentional, it's important to have code that highlights it by default.
> Screen real estate is limited, especially vertical real estate.

meh, my ide squashes short if `err != nil` clauses (goland, but I've seen other editors/ides/golang plugins do this as well ), also I run a vertical monitor. It's just not enough of an issue to care about. I've seen similar features for editors for other languages that have features or patterns that also create a lot of 'extra bullshit that takes up screen realestate'.

> no idea why people get so worked up about character counts

Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.

You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages. Quantity has a quality all its own, and Go does bog you down with its sheer line count. I'm not one who often appeals to this argument, by the way; the only other language I've done significant work in that I would apply it to would be C. It's very common to write bloated, information-poor Java code, but that is still a choice, even if it is the most popular one.

My reaction looking at Go initially was that it was exciting to have a fast, simple language designed for writing services. My reaction to reading and writing code of real applications has been that Go is badly suited for writing nontrivial application logic.

> how often one experiences exception traces when using an application written in Python or Java

Python and Java aren't particularly ambitious standards for a 21st-century language.

> Think of reading code as mining ore. If the ore is rich, you don't have to mine and process nearly as much of it to get the material you need. If the ore is poor, you have to invest extra effort to mine more ore to get the same amount of refined material.

Reading code and mining have nothing in common. In particular, mining technology works best on dense ore, human visual perception requires whitespace to operate efficiently.

> You might think Go is easy to read because lines are individually very easy to read, but Go code is so information-poor (partly because of error handling boilerplate) that you have to read a lot more lines of it to understand what a system does compared to other languages.

I think Go is easy to read because (1) it ranks at the top in my experiences with other languages and (2) because humans are very good at scanning visual structure and less good at parsing arbitrary syntax. Most languages tacitly acknowledge (2) by way of indentation and other syntactically irrelevant whitespace, but they don't apply the same rigor to error handling.

> Python and Java aren't particularly ambitious standards for a 21st-century language.

I was remarking specifically about exception handling. Has there been much innovation in exceptions among 21st-century languages?

I don't think the difference between Go and more expressive languages is about whitespace. If the only way a language achieved fewer lines of code was by cramming more characters into a line, I wouldn't give it any credit for that..

> Has there been much innovation in exceptions among 21st-century languages?

Yes, there has, mostly in the ability to use exceptions less than previously. With Java (at least old-school Java, not sure where it is now) exceptions are the only type safe language-supported way for a function to terminate with multiple types. If a function has multiple possible return types that don't have an inheritance relationship, you can choose between 1) returning Object and dynamically checking for the specific types, 2) defining a return class with a field for each possibility, or 3) picking one type to be the "expected" outcome and defining all other outcomes as exceptional. With 1) you lose many of the benefits of static type-checking; with 2) you get code bloat; with 3) you get the hazards of using a nonlocal control mechanism when you don't want that power.

If your language has sum types, you have a better option for those situations.

Cognitive complexity is not related to character count.

The expression

    let x = f.a()?.b()?;
is exactly as "easy" to parse as the code block of

    y, err := f.a()
    if err != nil {
        return fmt.Errorf("a: %w", err)
    }

    x, err := y.b()
    if err != nil {
        return fmt.Errorf("b: %w", err)
    }
They are effectively equivalent in terms of cognitive load.
I disagree. These two bits of code cater to different ways of reading. The first caters to a happy-path reading, where the reader has the choice to yadda-yadda the error handling or mentally expand it. The second foregrounds the error handling on an equal footing with the other logic.

I like your example, because this is exactly what happened in the application code I had to work with. In an application with complicated business logic, it isn't just one line of code turning into ten like you have here. It's ten lines of business logic turning into forty or fifty, where each operation is separated from the next by multiple lines of error-handling boilerplate.

The trade-off is that in Go code you can see every error path. This is a good trade-off for systems where absolute reliability and rigorous error handling are critical.

In an exception-oriented language the error paths are often invisible. This is a good trade-off for complicated business logic where error handling usually means aborting with an appropriate exception. Think about processing a record or a request where you have to validate the request, look up a few related objects in a datastore, check some business rules, do an authorization check for the requesting user, calculate the result of the request, store the result in a datastore, and produce a response. Each step can be written in a couple of lines of code that are hopefully pretty understandable if you have good names, like this:

    validate_request(request)
    user = fetch_user(request.for_user_id)
    authorize_user(user, Privileges.CanFoo)
    dingles = fetch_dingles_by_dongle_group(request.dongle_group_id)
    unfooable_dingles = dingles.filter(not_fooable)
    if (!unfooable_dingles.is_empty()) {
        throw BadRequestException("This dongle group contains unfooable dingles")
    }
    fooment = calculate_total_fooment(dingles)
    fooment_store.save(fooment)
    return DongleGroupFooed(request.dongle_group_id)
From one point of view, this code is nice. You can read these lines of code quickly and see what the basic request handling logic is. It reads like a story.

From another point of view, this code is terrible. A lot of different things can go wrong here, and only one of them is visible. What happens if the user can't be found? What happens if the user isn't authorized? What happens if the dongle group id can't be found? If the wrong exception is thrown, the wrong result will be reported for this request. You have to navigate to other functions to check that. If that makes it bad code for you, then you'd probably rather be writing Go. In Go, these eleven lines would turn into thirty to fifty lines of code. The handling of each error would be visible, at the cost of the happy path being harder to follow.

You're absolutely spot-on with this analysis. And it is a core assertion of Go that non-explicit error handling produces less reliable programs. It's a value judgment and it's a subjective assessment.
The solution space here isn't "go vs exceptions," it's "errors as values vs exceptions (vs "let it crash" vs...)," and Go isn't the only implementation of errors as values.
Of course. I wasn't trying to imply anything to the contrary. But given the prevalence of exceptions, if Go's error handling is performing on par or better, then it seems pretty ridiculous to characterize Go's error handling as "shit". Don't worry Steve, I think Rust's error handling is pretty cool!
I do agree that exceptions feel like the worst of the various bits of the problem space, to me, but just to be extra clear about it, I have never written a significant amount of Go, and therefore don't really have a very strong opinion about its error handling.

And error handling is such a huge and interesting problem space! I've long wondered about why I didn't like checked exceptions in Java but do like errors as values, for example.

I'll take checked exceptions over Go's multiple return values error convention any day.
Agreed! For something as pedestrian as error handling, it's always surprising to me how much it seems there remains to explore.
Character count does matter, but I still don't see how it makes code more readable if all the significant operations are supposed to happen in the condition block of an if statement.