Proposal: A built-in Go error check function, try
github.com
github.com
Here are some examples of me using it in error-check-heavy functions: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13
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
} 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 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.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.
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.
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.
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 :)
func CopyFile(src, dst string) (err error) {
^^^
hereFrom 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/...
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...
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.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.
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.
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...
Also, you don’t use try everywhere, that’s the point. They could do with some better examples.
So like, checked exceptions?
>Also the error is in the function signature, unlike exceptions.
So like, checked exceptions?
For one, the Go team hasn't invented exceptions, so they don't fit the NIH goals.
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
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.
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.
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.
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.
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.
Which is not bad at all.
In fact, it's closer to how e.g. Erlang works.
>> without `go fmt` "fixing" it
Great point.And this line should be indented relative to the 'happy path' lines!
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
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.
And what? Why avoid exceptions?
It would be nice to skip directly to `try func() error{}()` and avoid that specific pain.
Or, you know, exceptions are the right slope, and the ad-hoc implementations of generics, error handling etc, is the slippery slope...
it is amazing the speed at which a language can go from something elegant to a mess.
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.
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.
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.
So yeah, I did it for around 20 years, and don't miss it.
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.
- 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.
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?
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.
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.
That's an utterly pointless derailment.
Why makes you think everybody should be uppity and approving about Golang? Some of us feel is a step in the wrong direction.
I'd think anyone who creates a language is bound to be opinionated about what should be in a language, no?
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.
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.
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.
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.
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.
https://www.cvedetails.com/vulnerability-list/opmemc-1/memor...
Then you should advise Google on what they are doing wrong.
"The State of Kernel Self Protection Project by Kees Cook, Google"
Should `try` wraps the error and adds something more useful for debugging purpose? (the line number probably?)
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.
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)
}
}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.
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?
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.
Details here: https://dev.to/deanveloper/go-2-draft-error-handling-3loo
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.
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=...
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.
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
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)
This form puts to the background that though, and I fear I feel tempted to put try everywhere.
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 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?
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.
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!
https://go.googlesource.com/proposal/+/master/design/go2draf...
Doesn't look like the proposal adds anything new.