I appreciate this change of heart, as I wouldn't want a half-baked solution to make it into Go, but I hope they don't just stop there.<p>What the try() proposal gets wrong is that it tries to automatically bounce the error back the stack, being equivalent to "if err != nil { return }", which is ofte reasonable, but at the same time lacks the flexibility that current Go code has in terms of augmenting the error, or indeed handling it: You'd end up with try() calls for <i>some</i> things, but not all, so it's just ironing out one wrinkle and not every wrinkle. The second argument to try() is too heavy-handed. What we need is easy, readable syntax for all cases.<p>In current Go code, people like the "v, err := someFunc()" syntax because it allows the happy path to stay in the current scope, with the secondary syntax "if v, err :=" introducing a new scope to avoid polluting the parent scope. If you're in the current scope, your code stays flat and clean (although Go's love of shadowing cause subtle).<p>In my opinion, we need a syntax that is similar to a "catch" block in other languages, but easier. Perhaps something like this:<p><pre><code> v := getName() check err: {
return errors.Wrap(err, "could not get name")
}
</code></pre>
This allows you to handle the error and augment it, while not introducing anything magical. It's exactly equivalent to an "if v, err :=", but without introducing a new scope <i>and</i> not polluting the current scope with an error variable. This syntax would happily support fallthrough:<p><pre><code> v := getName() check err: {
log.Printf("could not get name, ignoring: %s", err)
}
// v is now the zero value
</code></pre>
And of course you could still support a syntax for introducing a new scope:<p><pre><code> if v := getName() {
// v is now valid
} check err: {
// You can return or anything else
}
</code></pre>
You could easily extend it to error types with some kind of matching syntax:<p><pre><code> v := getName() check err == io.EOF {
// On EOF
} check err: {
// All other errors
}
</code></pre>
The above syntaxes don't support chaining. I'm on the fence. I like Rust's "?" postfix syntax, and I think it could work:<p><pre><code> // This could be "monadic"; the first error short-circuits
ceo := getPerson()?.getCompany()?.getCEO() check err: {
return errors.Wrap(err, "couldn't get CEO")
}
</code></pre>
But why not just let "." be smart about errors and fall through like this?<p><pre><code> ceo := getPerson().getCompany().getCEO() check err: {
return errors.Wrap(err, "couldn't get CEO")
}
</code></pre>
After all, "." can know if the function returns multiple values, and "." on a tuple (or whatever Go calls it) isn't valid, so why not "promote" the "." to a high-level operator here?<p>The awkwardness of the try proposal goes deeper than just error handling, too, I think. One reason Go can't solve its error handling problem <i>elegantly</i> is because of its reliance of multiple, exclusive return types. Almost all functions with this signature:<p><pre><code> func getName() (string, error)
</code></pre>
...obey the unwritten rule that the function returns <i>either</i> a valid value, or an error. If you get an error, the value is supposed to be unusable, and the way to check for it is to look at whether there was an error.†<p>In many type systems, a situation where a return value can be <i>either</i> A or B is variously called a union, or discriminated union, or enum, or sum type, which is the term I prefer. It's always bugged me that Go's designers didn't go one step further and made sum types a first-class citizen, because I think it would fit Go rather nicely. TypeScript solves it this way:<p><pre><code> function getName(): string | number
</code></pre>
And you can also define types this way:<p><pre><code> type number = int | float64
</code></pre>
This, incidentally, introduces the idea of optional values:<p><pre><code> function takesOptional(x: string | null)
</code></pre>
Back to error handling, it would be more natural for a function to declare itself this way:<p><pre><code> func getName() string | error
</code></pre>
After all, it returns a value <i>or</i> an error. It can never be the superposition of both.<p>In this regime, anything that gets a value needs to explicitly check for what it is. "switch" on type would work, just like today, but that gets verbose for mere errors. So we just say that the "check" syntax as outlined above would work for any union that involves an error. In other words:<p><pre><code> func getThing() Person | Company | error {
...
}
thing := getThing() check err: {
return errors.Wrap(err, "can't get thing")
}
</code></pre>
We can do this because we can say that "error" is special and known to the compiler, just like make() etc. are special.<p>Of course, sum types go beyond errors, and open other avenues that are very poorly served by Go right now.<p>---<p>† This, unfortunately, isn't universally true. For example, io.Reader's Write() method's contract says that if it returns io.EOF, it may still have read a final amount of data into its buffer. Many get this wrong and ignore the data read. Which points to the problem of conventions that only <i>seem</i> like unwritten rules, but also highlights how the lack of strong type-system support creates the issue in the first place.