Hacker News new | past | comments | ask | show | jobs | submit login

I used to write code like that during the 80 and 90's, until settling down in languages with first class support for exceptions.

So yeah, I did it for around 20 years, and don't miss it.




I don’t like exceptions at all. They do not make it obvious what is going on, I never am completely sure if I’m handling them right. When I want to throw an exception I’m often unsure which would be right, and sometimes your API can have multiple reasons to throw the same exception.

Go error handling is not like that. But I can sure as hell say fairly that Go error handling is likely also not similar to what you did for 20 years in the 80s and 90s either. Rob Pike and friends surely knew a lot about what programming was like at that time. I, being relatively a youngster, don’t first hand, but I can tell you my experiences with C++, PHP, Python have not been nearly as good as Go with error handling.

For one thing, C++ has no rigid standard for how to handle errors. Some people use exceptions, some used error methods on classes (including the standard library,) some used special integer or enumeration values (...including the standard library,) and some had libraries and frameworks have their own magic error handling mechanisms. This cognitive overhead was horrible. C wasn’t much better; atoi is a case study in why error handling in C sucks. Libraries that tried to standardize it, like SDL, were bearable if it was all you used, but it probably wasn’t, and some APIs, like Win32, made it even worse. (And I suppose it is worth at least mentioning setjmp/longjmp error handling. I don’t think it’s necessary to comment on why it’s not good.)

Python exception handling is admittedly better, but its not really wonderful. Exception handling code in Python is prone to breakage that is first detected at runtime. If a function implementation changes, and the set of exceptions it might throw changes, that’s an invisible API change that may cause an unhandled exception in production. Not so great. Also, on a vaguely related note, you can’t really do error values using multiple returns like in Go, because Python doesn’t support multiple returns, only tuples, and refactoring between returning values and tuples is likely to run into accidental runtime errors (though you can paper over this issue a bit with type checking.)

PHP error handling sucks, I will withhold from elaborating.

All of the exception handling mechanisms suffer from one problem I really don’t like: it’s another nearly invisible part of the API. It makes the wrong thing (not handling errors) easy, and the right things (handling the appropriate errors correctly) hard. Your dependencies have to care about your call tree, and if it changes in refactoring it’s anyones bet what kinds of exceptions your function might throw. You could catch all exceptions, but because language errors like syntax errors (JavaScript) and index out of range and property name errors (Python) can also be exceptions, you rarely want to catch all exceptions. Not to mention, your call itself would be caught, so any exceptions caused by anything else in the try block would also be conflated.

Go does some things that are mostly not new, but haven’t all been packaged together this way before exactly:

- Custom errors via implicit interfaces, allowing easy, arbitrary data to be passed through errors while maintaining full control of error messages

- Deep separation of programming errors and operational errors tend to be passed as error values. Programming errors, like indices being out of bound or misusing an API, typically results in a panic, whereas operational errors. Very seldom do you actually care what the error is, but when you do you can inspect the error as any other value, because it is just any other value.

- Ecosystem-wide standards for how to pass errors. It’s almost 100% universal that errors are passed at the end of the return list. This makes it easy to parse for humans and easy to lint for machines. Linters can warn you about unused error values, and if a function suddenly has an error return it’s an API break, forcing you to fix existing code to properly handle errors.

- Good library support for error types, including fmt.Errorf for one-off errors that don’t need special handling, and (third party) a myriad of error wrapping/helper libraries. They’re not needed at all, but can be quite handy.

It works a lot better imo. You have more ceremony but less guessing. You can read a function and see almost every edge case, and when all of the functions perform good error hygiene you no longer need to guess about what refactoring your code will do.

I’ll take my repetition.


Yeah, until one realizes that most Go error handling is a mix of

- Parsing error results inside strings

- if .... else boilerplate

- Underscore everywhere to silence them

- Abuse from panic, aka exceptions in disguise

If you want error handling without exception's guesswork, there are checked exceptions (used for the first time in CLU 1975), and result types (used for the first in ML in 1973)

Both without the ceremony that Go shares with Algol derived languages before exceptions were a thing.

Rob Pike and friends surely do know a lot about what programming was like at that time, but they are also very opinionated on what they impose on others.

Just because they have a very good career, it doesn't make them always right.

I tend to think for myself and not from opinions of others.


Parsing error results inside strings

Fair point - this is a nasty result of their simple interface, and I think a mistake given much of the std lib doesn't use concrete types behind the interface. fmt.Errorf leads directly to this. It's simple, but not very flexible and can lead to horrible habits like parsing strings.

if .... else boilerplate

It's more like if boilerplate. People don't tend to use else unless absolutely necessary - it's if err deal with it, otherwise proceed. I agree errors in Go are more verbose than in some other languages, though in practice I don't find this a huge problem. I think I'd prefer result types but you're still handling it in a similar way.

Underscore everywhere to silence them Abuse from panic, aka exceptions in disguise

This is not how Go is used in practice, I mean I'm sure somebody somewhere does this but it is not the norm. I maintain large codebases at work with zero instances of these faults.


> parsing error results inside strings

I don't know why this is a thing. I create an explicit type for errors that I know I'm going to do something with, and do type assertions to check for them. I believe this was the intention behind the design in the first place.


Because many libraries (including stdlib) return basic errors which you can do nothing else with.

I think that's the main problem with this design - you're reliant on others to produce errors you can reliably check against, and every library could potentially have its own error types (similar to the problems with a proliferation of exception types in languages using exceptions). When using your own libraries it is not a huge problem - as you say you can start using a more complex type.

Not sure what the answer is, but the error interface which only allows returning a string is partly to blame IMO.


> exceptions in disguise

In disguise? I do not believe there is any mystery that panic/recover provide exceptions in Go. An exceptional case is exactly when you would want to use panic in a Go program. panic is provided in the language specifically to deal with exceptions.

If anything, it is other programming languages where exceptions are usuals in disguise.


You've read a lot of very bad Go code then, that doesn't follow any of the established practices for it.

Your list there is almost a primer of "what not to do in Go", and is certainly not representative of the Go code in e.g. the standard library.

I sense some frustration about the language... what happened to make you so anti-Go?


You mean like this standard library code?

https://go.googlesource.com/proposal/+/master/design/go2draf...

> I sense some frustration about the language... what happened to make you so anti-Go?

Go is C with GC and bounds checking, aka Limbo reborn with some Oberon-2 influence.

Already much better for our IT safety than sticking with C, still I kind of expected Google capable of producing Swift, Rust or TypeScript level of language design, given their pile of PhDs.


that's not standard library code? that's a link to one of the few thousand proposals for changing Go error handling... I'm not sure what you're trying to say here...

I think keeping Go this simple was an extremely hard thing to do for the designers. I don't know what the intentions were for Swift or Rust, but the Go team were always pretty straightforward that what they wanted was a safer C to write servers in. I think we all agree that they achieved that.


>I sense some frustration about the language... what happened to make you so anti-Go?

Why makes you think everybody should be uppity and approving about Golang? Some of us feel is a step in the wrong direction.


> I sense some frustration about the language... what happened to make you so anti-Go?

That's an utterly pointless derailment.


true, but I was curious


> but they are also very opinionated on what they impose on others.

I'd think anyone who creates a language is bound to be opinionated about what should be in a language, no?


>- Parsing error results inside strings

You do not need to parse error results in strings. Many libraries expose structural error types which can be inspected with type switches or other language mechanisms. However, for the most part, you can just treat all errors from a function the same.

There's definitely cases where you can't, like you may want to detect whether or not your error should be retried or treated as a permanent failure. You do the same thing you'd do with an exception: type-switch on the type of error, just like you'd catch on the type of exception. You can also do value comparisons for some errors that are constant, like io.EOF, which is handy in simple cases. The standard library also has some helpers for a couple common cases, like os.IsNotExist for checking if a file error occurred because the file did not exist.

The only time where you really, genuinely would need to parse strings is if you caught panics from the language runtime, which are actually just strings. However, this is unsupported, and the current Go HEAD actually just changed the format of an index out of bounds panic, so it would be very unwise to do this.

Examples of libraries that provide richer errors that satisfy the standard error interface:

- go-pg: https://godoc.org/github.com/go-pg/pg#Error

- elastigo: https://godoc.org/github.com/mattbaird/elastigo/lib#ESError

- redigo: https://godoc.org/github.com/gomodule/redigo/redis#Error (it's a string because the underlying protocol uses error strings.)

- gin: https://godoc.org/github.com/gin-gonic/gin#Error

The first three I picked because I used them, but the last one was fun. I just found one of the top Go libraries on GitHub explore and checked to see if they had a rich error type in their library, and they did. It's definitely common practice.

So yeah, you shouldn't be parsing error strings.

>- if .... else boilerplate

Yes that's the repetition problem that there's proposals to fix, but if that's the worst problem I still find it less annoying than needing this, which requires at least two new scopes:

    try {
        doThing(param[0]);
    } catch(e IOException) {
        Log.Warning(e.message);
        return;
    } catch(e ApiException) {
        throw new InvalidParameterException(String.Format("Invalid parameter: {}", param[0]), e);
    }
Or, even worse, not needing anything at all.

    //  Compiles
    //  No lint warning
    //  Sometimes correct!
    doThing(param[0]);
...Which is not always even bad practice because you may very well want the parent to catch those. But without comments, there's no way for the users of your function to know what to catch unless they inspect the function.

Without inspecting every possible codepath, it is impossible to know which errors are inadvertently not handled, and sometimes it is difficult to tell how a given error will be handled.

The correct thing to do in Go is almost always some variation of this, which is pretty simple:

    err := doThing(param[0])
    if err != nil {
        return err
    }
But these blocks are not invisible, and sometimes it will occur to you while writing it out that it isn't right for a given call site. So you can make it more complicated:

    err := doThing(param[0])
    if err == io.EOF {
        log.Printf("While doing thing: %v", err)
        return err
    } else if err != nil {
        return errors.Wrap(err, fmt.Sprintf("invalid parameter: %v", param[0]))
    }
Go doesn't force correct error handling, but it makes incorrect error handling more obvious, and it certainly makes you aware of error paths.

>- Underscore everywhere to silence them

You shouldn't silence them, unless maybe if you are writing example code. But a lot of examples on the web will just have correct error handling, which seems like a win/win to me.

>- Abuse from panic, aka exceptions in disguise

That's just bad code. You even said 'abuse' yourself.

You can call them exceptions in disguise, but they're really not. This thinking basically implies all error handling that unwinds the stack is “exceptions.” If they were exceptions, presumably you'd love Go error handling, because it has exceptions. Panic is a lot more limited, and generally good software will only catch panics in a couple types of circumstances:

- At API boundary edges when dealing with an API that nests deeply. In this case, you can recover but panic if the error was not an API error. This would allow a library to avoid passing the error value around when the only logical thing to do with the error value is to pass it back to the library user; A good example would be a parser.

- When trying to isolate a failure, for example to prevent one HTTP request from taking down an entire HTTP server.

>If you want error handling without exception's guesswork, there are checked exceptions (used for the first time in CLU 1975), and result types (used for the first in ML in 1973)

Does your language of choice actually support checked exceptions? C#, Python, JavaScript don't. Only modern language I am aware of that does off-hand is Java, and I don't think very much at all uses it, because it is even more annoying than Go error handling.

>Just because they have a very good career, it doesn't make them always right.

No, but it would be awfully strange if they learned nothing from that experience, which is kind of what you implied.

>I tend to think for myself and not from opinions of others.

This is just an empty platitude.

I never claimed that my opinion of Go being good was due to the background of Rob Pike or Bell Labers in general, just pointing out that the point of 'I programmed in the 80s and 90s' seems kind of odd given the background of the language designers.

Go is very opinionated, but I happen to like those opinions, genuinely.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: