Hacker News new | past | comments | ask | show | jobs | submit login
Proposal: A built-in Go error check function, try (github.com)
131 points by stablemap 4 months ago | hide | past | web | favorite | 118 comments



I implemented try() in Go five years ago with an AST preprocessor and used it in real projects, it was pretty nice: https://github.com/lunixbochs/og

Here are some examples of me using it in error-check-heavy functions: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13


I wrote something similar but of lower quality about 5 years ago as well


If it were a keyword rather than a pseudo-function, I would support its addition. Psuedo-functions should not be allowed into the language, as they tend to have side effects that you wouldn't expect from a function call (such as returning from caller to grandparent).

Some keyword that implies "return if error" so that you could then do:

    try v1, v2, v3 := someFunction()
You could even make it a little bit smart, taking advantage of the return types of the function to automatically fill in named return values:

    func MyFunc(param int) (result int, err error) {
        result = 0
        if param > 0 {
            result = param + 10
        } else {
            try p1, p2 := GetInternalValues(param)
            result = p1 * p2
        }
        return result, err
    }
where "try p1, p2 := GetInternalValues(param)" is syntactic sugar for (in this case):

    var p1, p2 int
    p1, p2, err = GetInternalValues(param)
    if err != nil {
        return result, err
    }


Given that putting 'try' before the variable definitions would probably interfere with the 'var' keyword, it makes a bit more sense to me that it should be put after the assignment operator (something like var try v1 = Thing() is probably lexable consistently, but it would be confusing to remember if it goes var try v1 or try var v1, unless it supported both which seems ish)

    v1 := try someFunction() 
I like that, primarily because it has echos of how the go keyword already operates. But the important thing is that it should be disallowed anywhere that isn't directly next to an assignment, which might be difficult. Like

    // compiler error
    fmt.Printf(try someFunction())
Not sure how easily that can be enforced without affecting the lexability of the grammar or compilation speed, but in a way, it should be thought of as a tokenized part of the assignment, not the RH-expression, such that the assignment tokens become

    =
    :=
    = try
    := try


I am strongly against this. `try` seems exactly like a function yet it is not acting like a function at all. People wouldn't expecting calling a function may return from the caller. And there is a reason why golang doesn't have macros. With macros all kind of craziness would be possible, and would really difficult to read different kind of projects' code.


Agreed - I thought this looked pretty reasonable, if a bit parenthesis-heavy, until I saw this example:

    func printSum(a, b string) error {
        fmt.Println(
                "result:",
                try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
        )
        return nil
    }
When you nest the calls to try inside another method call, like this, the control flow really becomes obscured.


I dislike this proposal for the same reason. Looks like a function but does not behave like a normal one. I personally don't mind the verbose error handling in go and I think it's clear and easy to follow.


> People wouldn't expecting calling a function may return from the caller.

Why? This is essentially the same as the reset/shift pattern in e.g. Scheme - when you call shift from within a reset call, that might cause you to return directly from that. And that is a rather well-behaved pattern which composes quite well, it's nothing like the inherent weirdness of, e.g. call/cc.


Go kind of has macros, like everything else, Go's solution to first level features in other languages is called //go:generate.


Do you think it would be better if it was called try_or_return?


This is interesting, but I think it doesn't necessarily fit the language well.

1. Go has very few keywords relative to other newish languages like Swift and Kotlin. Introducing a new one should only be done if the benefits are undeniable.

2. It causes an early return without any use of the "return" keyword, which feels pretty weird.

3. It's a bit weird that err will be magically defined in a defer function if the surrounding function includes a "try". Does err have to be declared earlier in the function? If yes, it's strange that it seemingly never gets assigned. If there are multiple variables of type error in the function which one gets assigned the result of "try"?

4. Probably most importantly it doesn't feel very explicit. In some languages this may not be a problem, but Go is designed to be very explicit and this seems a bit incongruous with the rest the language's style.

Maybe I'm just not understanding the proposal. I do like how concise this is. It's nicely backwards compatible and allows existing error handling to stay the same.


For 3., For use in defer the error return does have to be declared as you'd expect, in the examples it's done in the function signature

  func CopyFile(src, dst string) (err error) {
                                  ^^^
                                  here


1. As explained in the proposal, try is not a keyword, but a built-in function.

2. Returning if there is an error is the whole point of this proposal.

3. I don't understand your point. Could you provide an example?

4. try is explicit. There is no implicit behavior or stack unwinding.


it uses the same syntax as functions, but it doesn't behave like a function – functions generally can't manipulate control flow like that. so calling it a function is weird because it's closer to a language construct like `await`


We already have one built-in function that alters the control flow: panic. But I agree it's a bit weird.


tbh i don't know Go too well. as i understand it, `panic` can indeed hijack the control flow, i.e. stop it. but any function¹ can do that: just do `while 1 {}`! control flow won't return to the caller either way. so it's not that weird after all

Ruby blocks can do non-local control flow, i.e. return from their surrounding function, but that's what makes them distinct from functions.

---

¹ in a turing-complete language

(i'm a Haskell enthusiast, so please excuse the nitpicking :)


A panic can be recovered with recover(), and thus the program can continue. While a function that does an infinite loop will halt the program and nothing else.


It's the try! macro from Rust! Obviously I'm a big fan of this style of error handling, and I'm happy to see it proposed for Go.


Except it’s missing the part where sum types are used, resulting in oddities about the non-error part of the return value when the error part is set.


I mean, sure, but it's the best they can do while remaining compatible with all the Go code out there.


try! is dead, long live try.


Well, try! is dead quite literally. Rust still has the macro, but it is superseded by the ? operator.


That's the pun :). "The king is dead long live the king" originally comes from the French court and actually was used to refer to the old king dying and the heir to become the new king. https://en.wikipedia.org/wiki/The_king_is_dead,_long_live_th...!


It looks like it’s basically a less hacky version of what the RETURN_IF_ERROR and ASSIGN_OR_RETURN macros[1] do in C++.

From experience, those work pretty well. That approach eliminates a lot of the boilerplate bookkeeping code, while still making it explicitly obvious to the reader that a given function call can fail.

This seems nice, and I would definitely use it.

[1] https://github.com/protocolbuffers/protobuf/blob/master/src/...


I get that the error is still propagated "manually" behind the scene; but how is this different from exceptions in practice once you use try everywhere (except where you forgot and the error is dropped silently)?

Here is my proposal: add restarts [0] as a complement to manual propagation.

[0] https://github.com/codr7/g-fu/blob/master/v1/doc/typical_res...


Exceptions are the opposite of a goto statement, basically a 'comes-from' statement. They violate the principal of least surprise in every way possible -- you may have no idea what type is being sent your way, where from or what to meaningfully do about it as a result. They often result in memory leaks in languages like C++ due to lack of destructor invocation.

try() errors can only propagate to the caller. As such, stack unwinding is clear, no memory issues arise, performance is good and locality of error is preserved. Surprise is minimized.

While it may look similar, it's pretty markedly different in important ways.


> you may have no idea what type is being sent your way, where from or what to meaningfully do about it as a result.

How is this different from (a) errors bubbling up with a pile of concatenated strings as the only type information or (b) errors not bubbling up because someone decided they would never make a mistake?

The whole value of exceptions, to me, is consistency. The error is guaranteed to propagate in a consistent way up the stack. Static type analysis has a fighting chance of predicting what could ever propagate up the stack, whether checked or unchecked.

Trying to work out out what `err` might be in various situations is an exercise in forensic grepping. Trying to react intelligently and reliably to different types of error is an exercise in hoping nobody changes the error string.


There's no need to unwind the stack to get what you want.

Restarts [0] evaluate the error handler in the throwing scope/environment and the only way out of there is invoking a predefined restart or aborting the program.

[0] https://github.com/codr7/g-fu/blob/master/v1/doc/typical_res...


I'm aware of, but have never used, the CL conditions/restarts mechanism. It seems amazing but I wonder if it would manage to seem foreign to everyone in one of these discussions.


Unchecked exceptions... checked ones are basically a bifunctor with a wacky syntax


The error can't go through a function that doesn't return an error, unlike exceptions. Also, you have to ask for this. Reversing the default is a big difference.


Unlike unchecked exceptions.


Even with checked exceptions, you have to be careful. You may not want exceptions from every function you call to fly up to the caller.

    function X() throws Oops {
        okToFail(); // fine if this failure goes up
        unexpectedFailure(); // not fine to go up
    }
Unfortunately, once you tell the compiler its ok for exeptions to go up, that rule applies to the whole function.


I’m sure that’s a common problem.


Because it doesn’t skip the call chain - each function must explicitly handle the error or pass it on. In practice usually errors are handled one or two levels up, not with some global error handler as people do with exceptions. Also the error is in the function signature, unlike exceptions.

Also, you don’t use try everywhere, that’s the point. They could do with some better examples.


>Because it doesn’t skip the call chain - each function must explicitly handle the error or pass it on

So like, checked exceptions?

>Also the error is in the function signature, unlike exceptions.

So like, checked exceptions?


Sure, there are different ways of doing this. But the OP didn't say 'checked exceptions'.


>I get that the error is still propagated "manually" behind the scene; but how is this different from exceptions in practice once you use try everywhere

For one, the Go team hasn't invented exceptions, so they don't fit the NIH goals.


So basically they're proposing:

  f, err := os.Open(filename)
  if err != nil {
    return …, err  // zero values for other results, if any
  }
can be simplified to

  f := try(os.Open(filename))
This makes a lot of sense, but I'm of two minds. On one hand, it makes things much cleaner. On the other hand, it might be a first step onto a slippery slope that ends with exceptions.

A lot of others chiming in with different ideas on the original ticket: https://github.com/golang/go/issues/32437


After chewing on this for a while, I've come to the conclusion that the thing exceptions does wrong (or if that is too controversial, substitute "most dangerously") is that it disconnects handling the error from the scope that generated it. It's the way exceptions so easily fly up the stack into code that can't understand them because it is too distant in context that is the problem. Neither this proposal, nor any other I saw, would change that. I wouldn't even call this "a step in the direction of exceptions"; it's a neutral move. Everything is still occurring within the same scope, it's just getting spelled differently.

Interestingly, this means that while pervasive use of "if err != nil { return err }" is technically very similar to exceptions when used everywhere, it can still have a different semantic meaning to a human reading the source.


Every time I switch back to exception languages, I get this tendency to "assume everything succeeds all the time, and handle it at the very top level in case any part of it fails". I do not think about what can go wrong at each level nearly as much as I do when I am required to use `if err != nil` soup.


> Every time I switch back to exception languages, I get this tendency to "assume everything succeeds all the time, and handle it at the very top level in case any part of it fails"

See, I get the exact opposite tendency. Exceptions mean that any statement, no matter how seemingly trivial from the caller's perspective, can fail. It means I'm constantly thinking "If an exception is thrown between X() and Y(), does it break any assumptions I've made about the program state?". You end up with hacks like putting code in finally blocks just to prevent partial execution.

I think exceptions would work fine in a language that also had built-in support for transactional memory, where you could commit or rollback operations when an exception is thrown partway through. Without any language support, though, I think that exceptions and mutable state do not play nicely with one another.


> Every time I switch back to exception languages, I get this tendency to "assume everything succeeds all the time, and handle it at the very top level in case any part of it fails". I do not think about what can go wrong at each level nearly as much as I do when I am required to use `if err != nil` soup.

This is generally the best way to deal with errors. In most cases, only the original caller really knows which errors ought to be considered fatal, and which should result in retries or be handled in some other way.

If you handle errors deep down in the call stack, you are robbing the caller of the ability to decide how to handle them. Different callers may want different things. It is better to let errors bubble up as far as possible.


Exactly. And in most cases I don't care. Can't open that file? That's a fatal error. Can't connect to the DB? That's a fatal error. Some file system operation failed? That's a fatal error.

In the majority of applications it's normally pretty obvious which errors you're likely to care about (the user already exists, etc.) vs the rest that you just handle at the top level. The trouble with go's error handling is it makes you care about everything, which is just a waste of time and effort because the majority of errors will be fatal anyway.


I completely agree. I love that go is _able_ to treat errors just like return values but most of the time I don't need to work with them like that. I want to assume the happy path and not have all the `if err != nil` noise.


Coming from Java I agree: that's what tends to happen, but I miss it when I don't have it.

It's helpful habit for services especially because most of what you're doing is local to a request anyway and you want to "undo" or "drop" things. Exceptions in services are great because they actually preserve situational information pretty well and a catch+rethrow is pretty good for annotations at different levels. Err/panic are so poor at capturing anything of value by default. They require a ton of diligence to be useful.

The nice thing about being able to catch exceptions is that I can kinda define what operations are "atomic" on my terms.

Presumptuous example disclaimer.

For instance, if I'm processing a set of files with records, I'll probably have a substantial error domain around the processing of a record, of a whole file, and my whole program. I don't want to babysit each byte read.

Now, I admit, this does require you to write code that didn't pollute your state with partial work. Unwinding from a frame/function should leave everything in a known place.


Yes, I like that part of exceptions. I can hold more of the problem in my mind when I don’t need to worry about what happens when something exceptional happens.


>I get this tendency to "assume everything succeeds all the time, and handle it at the very top level in case any part of it fails"

Which is not bad at all.

In fact, it's closer to how e.g. Erlang works.


on a side note, if I could just write `if err != nil return err` on a single line (without `go fmt` "fixing" it) that might be enough for me not to need `try`.


  >> without `go fmt` "fixing" it
Great point.

And this line should be indented relative to the 'happy path' lines!


> On the other hand, it might be a first step onto a slippery slope that ends with exceptions.

Rust originally had a try! macro just like go and is slowly sliding down that very slope. Let's just say there is a split in the community about whether an exception like outside syntax is a good idea or not. Some like the idea, some don't like it. It yields in threads like this one: https://github.com/rust-lang/rfcs/pull/2426


> it might be a first step onto a slippery slope that ends with exceptions.

Like that “slippery slope” with code generators leading to generics?

To me it seems like golang-users actually wants all those features it’s language-designers took away.


> it might be a first step onto a slippery slope that ends with exceptions.

And what? Why avoid exceptions?


>On the other hand, it might be a first step onto a slippery slope that ends with exceptions.

Or, you know, exceptions are the right slope, and the ad-hoc implementations of generics, error handling etc, is the slippery slope...


This is certainly following the path of JavaScript Promises which do, as you say, end in exceptions.

It would be nice to skip directly to `try func() error{}()` and avoid that specific pain.


I used to be a bit skeptical on the slow pace at which go decided to add new feature, and the insane care they took to have orthogonal features, but after seing the recent swift language evolution ( with google team pulling it toward dynamic features, and now apple adding weird and clumsy DSL support) i must say i’m now completely supporting their choice. taking a year before settling on a less ambitious but more orthogonal and minimalist feature is a sign of wisdom.

it is amazing the speed at which a language can go from something elegant to a mess.


Error handling is, to me, the most important thing when writing any code.

Promoting "if err != nil { return }" even more by giving it a keyword seems like a dangerous road to walk down.

Are we proposing solutions to make developers not have to type "if err != nil" or are we looking at how we can help developers handle errors better and be more productive?

I honestly don't have anything to offer as far as ideas for changes to the language, but I really hope we think through the long-term ramifications of these changes.


I think the way Go error handling works is fantastic, and one of the few problems it has is repetition. There’s C++ macros for various environments that let you do the assign-or-propagate-error stuff with a bit less repetition, but I dislike them because they feel opaque; people don’t often feel the potential consequences and just use it as a way to not think about error handling. With Go, the error handling behavior is so obvious and in your face that it’s painful. (This doesn’t stop bad error handling hygiene, but it has definitely helped me.)

Sadly, there’s no obvious dumb way to reduce that repetition. I would not mind a language mechanism similar to defer, like the handle/check proposal for example.

The most obvious issue I take with try is that it looks like a function call but is much more magical :( and also, I dislike named returns.


I've spent the last year working on a Go system, writing Go every day.

After a couple of months, I just stopped seeing/worrying about "if err != nil {". It has become punctuation, the Go equivalent of semicolons;

I don't even use snippets; I manually type that every time. Which is good, because it does make me think about whether this function call can error, and what I should do about it if it does. 90% of the time it's ("just pass it up"), 5% of the time it's ("nothing, I don't actually care if this routine fails") but 5% of the time it's ("right, yes, this needs to be dealt with here").

When reading code, I just skip over it if it doesn't do anything interesting.

I understand that in English reading, we don't notice "they said"; our brains just skip over it, and you can use "said" to open every quote and it won't feel repetitive. That's how I've got with "if err != nil {"

so... I'm against this "try" stuff, because it feels like it's been proposed by people who haven't worked with the language too much. I'm not against making it more friendly for new people to learn, but this doesn't feel like that. This just feels like "ugh, really? I have to handle all the errors?!"...which is not a reason to change it.


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.


So much this: "it does make me think about whether this function call can error, and what I should do about it if it does." This thinking is why C programs are the most reliable computing substrate - because errors can happen and there is no substitute for the programming taking a moment to think the possibilities of error. Build that thinking into the dev process, and you have a hope of reliability within the process (which doesn't mean anything else is reliable, but at least your call stack is.


Yeah, they are so reliable. A living proof of good quality.

https://www.cvedetails.com/vulnerability-list/opmemc-1/memor...


Linux hasn't segfaulted on me in years. Nor has Python C level (except when I use ctypes to use openssl directly). Nor has gcc, emacs or any of the standard shell commands I use. When I worked in a large C shop writing network servers, we all got emailed whenever our code segfaulted with a stack trace. Now our Java servers routinely catch all exceptions and then need to be restarted for some reason when the network glitches. I wouldn't use C for high velocity business logic ("alternating soft and hard layers") but I'd absolutely use it for stuff that needs reliability. I'd use Go now also.


Really?!?

Then you should advise Google on what they are doing wrong.

"The State of Kernel Self Protection Project by Kees Cook, Google"

https://www.youtube.com/watch?v=aMkCKeZ8xZw


If there are multiples `try` inside a single function, and we are debugging and want to know which call raises error, how can we do that?

Should `try` wraps the error and adds something more useful for debugging purpose? (the line number probably?)


Maybe don't use this try if you want more detailed info on specific errors? It's for using where you would use if != nil return err not in every instance. If you try to use it everywhere it won't work well.


Yeah, that was my first thought as well. I'm using the juju/errors library "return errors.Trace(err)", which annotates the error with the line number of the return.

That wouldn't work in a deferred function. Perhaps the compiler could make that possible somehow although I suspect that might conflict with the stated goal of improving the efficiency of defer due to "try" encouraging more people to use it.


A deferred function is called at the line where return would happen, so you can still access stack trace information required for errors.Trace(err) from inside a deferred function. You just need to go "one frame higher". (See the stack trace printed in: https://play.golang.org/p/Bpqdm8oWBF3).

As a result, I believe juju/errors could then be extended with a new function, to be used like this:

    func foobar(...) (..., err error) {
        defer errors.Tracify(&err)
        ...
    }
where:

    package juju/errors

    func Tracify(err *error) {
        if *err != nil {
            *err = TraceFromDefer(*err)
        }
    }


is it just me or does the word "try" seem to imply the opposite here? To me saying "try" is like saying "attempt to run this" like we have in a try/catch block. I feel like something like "expect" would more directly explain what this does.

Great functionality though. I'd be very happy to see this included no matter what it's named.

EDIT: This is answered in the FAQ section after I read further. Apparently it's because "try" is already a keyword. I still don't like it, but I get it.


See also the "continue" statement in C/Go/etc for something that does the opposite of what the word suggests.


I suspect the downside is that it'll promote blind propagation of errors.

In rust that's fine because the type system will document what error types can be returned.

In golang, it important that every error type that can be returned is manually documented. Otherwise, it's better to just panic, since nobody can handle unknown errors anyways..

Or am I missing something?


Not really. You can just check if “err != nil” and switch control flow on this.


I always worry about state and side effects after handling an error like that..


Please for the love of god no.

Go is awesome for its simplicity. Errors should not be abstracted out of handling convenience. Errors are just values either eliminate the need for the error or handle it like you would any other value.

Stop trying to make go work like every other language.


Agreed. This is the hill I'll die on. Boring error handling is what I specifically love about Go...


Out of interest, how about the proposed check and handle changes proprosed for v2?

Details here: https://dev.to/deanveloper/go-2-draft-error-handling-3loo


Still not a fan. These seem like schemes for people who are annoyed by errors, and just throw them over the wall... similar to say putting an entire python block in a try/except.

With any sufficiently large application you start to realize how awful that is, especially when something fails and the only log is 'EOF'.

I've learned to treat errors as first class citizens, because they are. I always add an annotation and stack entry via wrappers before returning them. Unless I'm missing something, that seems all but impossible with these schemes.


This isn’t a replacement for go error handling, it’s a complement. Sometimes you don’t need to do anything other than pass an error up the chain.


I would rather every developer takes a second while writing out “if err != nil” to decide how to add context or maybe return a new error with useful metadata.


It's not really about being lazy, sometimes there is nothing useful to add. I removed some error annotations the other day as they were just cluttering logs without adding usefully to an error for farther down the call stack. Sometimes you don't need to annotate/wrap.


Yeah that’s something you can decide too, while typing it out. It’s not about having extra context every time, it’s about deciding. Sometimes you want to ignore the error and take the zero value. Adding try to the language creates a new path of least resistance that decreases code quality.


You can't decide to avoid the boilerplate though at the moment, because there is no try. You can only type out the boilerplate, and read it, even if you don't really need it in that particular instance.

It would not reduce code quality if used in instances where it replaces return nil, err - which is what it is intended for. It's not intended to replace wrapped errors, or error handling, it's intended to replace 3 lines of boilerplate with one word - in the cases where you want to push the error one level up the call stack.

Having used go extensively for the last 5 years and built some large projects with it, I disagree this is not useful. I think it's a good proposal in the spirit of go and should be adopted.

I'd prefer this as a keyword myself, but of the 6 million instances of if err != nil on github, I think around half could be eliminated by this pattern, so I think it would be useful, if not overused.

https://github.com/search?l=Go&q=if+err+%21%3D+nil+%7B&type=...


Interestingly enough C++ has a recent proposal that converges to a similar design from the other direction (i.e. exceptions).

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p070...

It's still a lot more implicit than go's design, as it's meant to be compatible with regular exceptions.


I love Go, but error handling can become cumbersome. Though, I also think a lot of that feeling is because at the codebase I'm working on we return too many errors in a strange way.

We're returning pointers almost everywhere along with an error, even simple methods that could do fine without. But the n you feel obligated to check them instead of ignore them, just in case someone does change the implementation of the simple function.

I feel like in some of the projects I wrote for myself, I use a lot less error handling and then I don't mind the 'if err != nil' approach anymore.

I'm sure this `try` keyword will help deal with the pain from the codebase I'm working on, though it doesn't adress the root of our problem :(

To give an idea, in a function that does 5 other function calls, I need to check error returns 5 times. Every. Single. Line. That can't be the norm, right? :P


Your second line is the answer - Go is opinionated, Go expects you to do things in a certain way(a way that follows standard specs in most cases). If you start bydoing a workaround in your architecture, you will similarly have to start doing workarounds in your Go code and it becomes SUPER messy super quickly.Google can afford to follow standard specs slavishly because giant company with tons of money. Maybe you can't...

It's one of the pitfalls of using an opinionated language. I don't think introducing something like this will work because it is also a hack that will propagate other hacks in your code. Just return errors in a more standard way(or stop using Golang)


The interesting part for me about the current form is that it makes me think what should be the state on the rest of the ecosystem in case an error happens. That extra lines, as for experience, pays off quite fast.

This form puts to the background that though, and I fear I feel tempted to put try everywhere.


Don't do that then. This is just another tool in the toolbox, you don't have to use it at all. I think I'd use it in about half the cases where I return an error, in cases where I simply want to handle it one level up without further annotation.


Go is a managed language, and that management extends to how you are allowed to write the code. There aren't simply tools in the toolbox, fmt and lint enforce programming styles.

In this community, it's accepted that if you didn't run `go fmt` on your code before committing that someone else will do it for you. If you don't fix all of the linter errors, you can expect contributions to be rejected.

If try is adopted, we can expect to see the linter pushing its usage aggressively. The OP will be pressured to use it, and ultimately has no say in whether they use it less than the linter demands and social expectations for idiomatic usage compels.


I see no reason for the linter to recommend it unless you are using if err != nil {return err} - if you are, it is functionally the same, therefore no change and it would be recommended, which is fine IMO.

I don't really see the danger here - if you want to annotate errors properly, do so, if you want to respond in place (with a retry for example), do so, if you don't do either and just return the error (which is sometimes fine) yes the linter would recommend the shorter version.

Where's the problem?


Not a fan. I don't mind the boilerplate to be honest.


I think check/handle is much better than this.


Same. I don't like how this requires you to use named parameters and defer to add context to errors. It's way too easy to shadow err and I can see this causing a lot of pain in the future.


I agree. I think they are focussing too much on backwards compatibility here. Java made that mistake in the last decade, hopefully Go does not repeat it.


Well, yet another magic function.


It seems so ad hoc. It's not really design when one just papers over a very specific shortcoming.


I agree that overloading the semantics of function calls is ugly, but to play the devil's advocate, does it have to be "design" in that sense if it truly does address the shortcoming in a practically sufficient manner?

Aside from not appealing at all to my sense of esthetics, I have no qualms about this because it does address a problem that a lot of people have complained about in a way that I could get the idea within a couple of minutes of reading the proposal. That said, a keyword might have been nicer.


I suppose the difference between a "quick fix" in a language and in a codebase, is that the former will be crystalized forever.

Language designers seem to like to bang the drum about the ethos of "Orthogonal Features". I know Go's have, in presentations. But that seems to be cast off when the going gets tough. See also "Contracts" in Go generics, which is mostly the same thing as the existing Interfaces feature, but working around the fact that interfaces didn't have a good story for symbolic operators. Orthogonal? Smorthogonal!


Looks a lot like the already proposed check/handle keywords that was met with a lot of push back from the Go community:

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

Doesn't look like the proposal adds anything new.


This new proposal is explicitly a simplification of the previous check/handle proposal. And for the record, there was a lot push back, but also a lot of support for the check/handle proposal. Language design is an iterative process.


It is from a core team member, usually those proposals tend to win in the end, as already occurred a couple of times.


A little similar to this one, https://gist.github.com/yaxinlx/1e013fec0e3c2469f97074dbf5d2..., but with more considerations for details.


I hope the custom handler part is reconsidered. It is fine to 4 panic if the handler is nil or it returns nil.


It’s contrary to Go core values of simplicity and avoiding slow patterns (using defer statements for error handling). It seems like a net loss and I would expect the proposal to be rejected.


Why not simply using a "?" the way kotlin and rust do?




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

Search: