value, err := function()
> if there is an error return it
if err != nil { return err }
> otherwise give me the value
// rest of the code goes here
Not really.
If I want to do foo().bar().baz() it expands to six lines.
Rust's `?` operator on Result<T,E> types is flipping fantastic, puts all of the following to shame.
// can forget to check err
thing, err := getThing()
if err != nil {
panic(err)
}
// More verbose, now you could possible forget to assign thing
var thing Thing
if t, err := getThing(); err != nil {
panic(err)
} else {
thing = t
}
// What I end up doing half the time when I've got a string of many
// calls that may return err as a result of this
var whatIActuallyWant string
if first, err := getFirst(); err != nil {
return err
} else if second, err := doWith(first); err != nil {
return err
} else if final, err := doFinally(second); err != nil {
return err
} else {
whatIActuallyWant = final
}
It's actually to the point that in quite a few projects I've worked on I've added this: func [T] must(value T, err error) T {
if err != nil {
panic(err)
} else {
return value
}
}Isn't that what your tests are for? Linters aren't normally intended to stop you from creating undefined behaviour.
It is not like Rust negates the need for those tests. Remembering to handle an error is not sufficient. You also need to ensure that you handle it correctly and define a contract to ensure that the intent is documented for human consumption and remains handled correctly as changes are made. Rust is very much a language designed around testing like every other popular language.
What you do need to do is document how the function is intended to behave. If, for example, your function opens a file, you need to describe to other developers what is expected to happen when the file cannot be open.
"The compiler won't let me forget to handle the error" is not sufficient to answer that. That you need to handle the error is a reasonable assumption, but upon error... Should it return a subsequent error? Should it try to open a file on another device? Should it fall back to using a network resource? That is what you need to answer.
And tests are the way to answer it. It is quite straightforward to do so: You write a test that sees the file open failure occur and check that the expected result happened (it returned the right error, it returned the right result from the network resource, etc.). Other programmers can then read your example to understand what is expected of the function. This is as necessary in Rust as it is in Go as it is in any other language you are conceivably going to be using. Otherwise, once you are gone, how will anyone ever know what it is supposed to do? As changes occur through the ongoing development cycle, how will they ever ensure that they haven't broken away from your original intent?
So, once you've written the necessary tests – those that are equally necessary in Rust as in any other language – how, exactly, are you going to forget to handle the error? You can't! It's impossible.
I don't know why this silly thought persists. It is so painfully contrived. If one is a complete dummy who doesn't understand the software development process perhaps they can go out of their way to make it a problem, but if one is that much of dummy they won't be able to grasp the complexities of Rust anyway, so...
type errHandler struct {
err error
}
func (eh *errHandler) getFirst() string {
// stuff
if err { eh.err = err }
return result
}
func (eh *errHandler) doWith(input string) string {
if eh.err != nil {
return ""
}
//stuff
if err { eh.err = err }
return result
}
func (eh *errHandler) doFinally(input string) string {
if eh.err != nil {
return ""
}
//stuff
if err { eh.err = err }
return result
}
func (eh *errHandler) Err() error {
return eh.err
}
func main() {
eh := &errHandler{}
first := eh.getFirst()
second := eh.doWith(first)
final := eh.doFinally(second)
if err := eh.Err(); err != nil {
panic(err)
}
} func foo() (final int, err error) {
defer func() {
if e, ok := recover().(failure); ok {
err = e
} else {
panic(e)
}
}()
first := getFirst()
doWith(first)
final = doFinally()
return
}
encoding/json does it. It's okay if you understand the tradeoffs.But look at what you could have wrote:
func foo() (int, error) {
first, err := getFirst()
if err != nil {
return 0, ErrFirst
}
err = doWith(first)
if err != nil {
return 0, ErrDo
}
final, err := doFinally()
if err != nil {
return 0, ErrFinally
}
return final, nil
}
This one is actually quite nice to read, unlike the others, and provides a better experience for the caller too – which is arguably more important than all other attributes. func foo() (int, error) {
first := getFirst()?
doWith(first)?
return doFinally()
}
or this: func foo() (int, error) {
first := getFirst() % ErrFirst
doWith(first) % ErrDo
return doFinally() % ErrFinally
}
The first one is a significant upgrade over the exception version. It cuts out half the code and makes the early return points explicit.I think something similar to the second one is also nice to read, and it gives the same improved experience to the caller as your suggestion.
Albeit a contrived suggestion for the sake of brevity. In the real world you are going to need to write something more like:
first, err := getFirst()
var err1 *fooError
var err2 *barError
switch {
case errors.As(err, &err1):
return nil, FirstError1{err1.Blah()}
case errors.As(err, &err2):
return nil, FirstError2{err2.Meh()}
case errors.Is(err, io.EOF):
return nil, EOF{}
// ...
case err != nil:
return nil, FirstError{err}
}
And that is where eyes start to gloss over. The trouble with errors is that they quickly explode exponentially. Programmers long to distill all possible errors into one logical operation to not have to actually think about all the cases, since that is hard and programmers are lazy, but that is not sufficient for a lot of programming problems.The cutesy shortcuts like ? and % operators are fine for some classes of programming problems, to be sure, but there are numerous languages that are already designed for those classes of problems. Does Go even need to consider travelling into those spaces? In the original Go announcement it was made explicitly clear that it was designed for a very particular need and was never intended to be a general purpose programming language.
I'm certainly not the gatekeeper. If Go wants to move away from its roots and become the must-have language for the classes of problems where something like ? is a wonderful fit, so be it. But, from my point of view, putting energy into tackling the big problems is more interesting. There should be plenty of room for improvement in the above code without losing what it stands for. But that is going to require a lot more deep thought than I've seen put in and programmers are lazy, so...
> Does Go even need to consider travelling into those spaces?
Oh come on. Changing how one common piece of boilerplate is written is not travelling into new spaces or moving away from Go's roots.
I didn't know about this trick, thanks for sharing.
Do people actually do this? Is it included in the standard library? If not, should it be?
1. Errors quickly lose their usefulness if you simply pass them up the stack. Even just on the surface, if you don't even know where the error came from, good luck making sense of it. Other languages have tried to avoid that problem by having things like "sidecar" handlers that can handle the error elsewhere, but in the end that's just moving code around. You haven't actually solved the overhead of needing to do something with the error.
2. Errors simply passed up the stack are, more often than not, going to leak implementation details. Consider a function that fetches data from a SQL database, where the underlying operations produce a "SQLNoResults" error. If you let that flow through, callers are going to start to rely on it. Now, imagine new requirements dictate that you need to fetch the data from an HTTP service instead. If you continue to simply pass the error along, now callers are going to get a "HTTPNotFound" error instead, breaking their usage. Not a good situation.
2. There are places where you want to abstract your errors, but those places are not every function call or even most function calls.
Do you mean often you want multiple segments of code to all do the same thing on error? If there is only one segment then you well and truly have just moved things around.
Multiple segments all doing the same thing on error would give more justification to centralizing functionality, but at the same time if you have multiple segments of code all doing the same thing you've probably not thought your design through. Papering over design mistakes with language features is commonly done, but I'm not sure it is something to strive for.
> but those places are not every function call or even most function calls.
If the original error is your own you don't need to abstract it, but if you are passing your own errors through multiple levels of indirection you've, again, probably not thought your design through very well. Papering over design mistakes with language features is commonly done, but I'm not sure it is something to strive for.
If a segment has 6 function calls and you want the same error handling for each one, you can't get rid of the boilerplate with the current language.
> If the original error is your own
Assume the original error is not my own then.
My deepest sympathies for the person who has to respond to the resultant error once you've collected your paycheque and have moved on to the next project. Which of the six functions produced the error? Nobody knows. That may be all well and good for a contrived internet comment example, but if you write real code like that someone's life is soon going to become a living hell.
Every other language has recognized that you can't have the same error handling for each of the function calls. Even where they have special error handling semantics have special ways to ensure that the handling is different in each case. Why do you think it would work in Go?
> Assume the original error is not my own then.
Then you've forever hitched your horse to their code. A better or more performant replacement comes along in the future and you want to use it instead? Too bad. You can't without breaking your own API – and for what reason?
But, okay, we accept that you like to live life on the edge (or come from the Javascript world and thus don't know any better) and if the people using your functions start having breakage, too bad so sad. However, if you're just passing values through from another package, why are you really offering your callers in the first place? Why don't they just use the other package directly?
> Then you've forever hitched your horse to their code. A better or more performant replacement comes along in the future and you want to use it instead? Too bad. You can't without breaking your own API – and for what reason?
No, I did not say that. What I said is that the place to prevent that is not every function call. If my code goes 4 functions deep, I need at least one of them to handle errors I didn't cause, or convert them into my own errors for the sake of a stable API. But many of the other functions can pass errors through.
If it only calls one function and isn't part of the public API it is likely that you can get away with it. There is a time and place for that, but if that time and place is most of the time like your earlier comment indicated and something that can be counted as many in this comment... I'd like to see this codebase because I am highly skeptical that it is something anyone would ever want to work on[1].
If it calls two or more functions, then you're back to the "which function was it?" problem.
[1] And, as it happens, Google actually commissioned a study on how frequently that kind of code is actually written based on open source projects and other code they had access to when evaluating an error handling proposal. They found it to be an unusual case. It being "most" or "many" is definitely limited to within your works, not something applicable in general. There just might be a reason why your ways haven't caught on, but thrill me!
Nope, not in languages where you have a stacktrace attached.
> Errors simply passed up the stack are, more often than not, going to leak implementation details.
That's why in a good language you can just wrap them at the right level of abstraction.
Like you say, the stack trace needs to be attached. For that you, at very least, need a "sidecar" handler if not done so in the same execution path, as we discussed earlier. Did you, uh, forget to read the thread?
> That's why in a good language you can just wrap them at the right level of abstraction.
You can move the logic around, but you can't avoid it, as we discussed earlier. Did you, uh, forget to read the thread?
I just read it again, but I'm not sure what you mean. And sorry, I'm not familiar with that terminology (sidecar) but from the perspective of the user/developer, does it matter? As system or library developer, I don't need to do anything - I'll have the stacktrace available when I need it. There is extra code necessary. (one has to be mindful of the performance, but that's it)
> You can move the logic around, but you can't avoid it, as we discussed earlier. Did you, uh, forget to read the thread?
Doesn't have to do anything with moving logic around.
Let's say you are function foo and you call other functions and one of them is bar and it will fail with barError. Then, to avoid breaking your (= foo's) consumers if bar changes its internals, you simple wrap bar's error with your own. That can be as simple as doing `bar.mapError(barError -> fooError(cause = barError))` and that's it.
That is all I wanted to say.
Then, depending on the language, you stil don't have to repeatedly do "`if err != nil {}`" or so. There are enough alternatives, e.g. monadic error handling like in Haskell or macros like in Rust.
You were familiar with it earlier – you couldn't have sensibly replied otherwise. How did you manage to lose it in the meantime?
> That can be as simple as doing `bar.mapError(barError -> fooError(cause = barError))` and that's it.
At the end of the day is that really any different than: `err = errors.Join(MyError{}, err)`?
But you've still just moved logic around (e.g. into mapError/Join). You've not changed what needs to be done.
In the end, everything is machine code. You tell me if that is any different or not.
> But you've still just moved logic around (e.g. into mapError/Join)
To improve backwards compatibility, yeah. Somehow the error needs to be changed.
But: with a stacktrace and a good language, this is a single line of code. No if/else etc. needed, even in the case of multiple different errors in different places in foo.
And my impression was that this is what we were discussing here - ergonomics of error handling.
Code is ultimately written for humans, not machines. If we only cared about the machine you could flip toggle switches and not worry about all these pesky human problems found in understanding code.
> You tell me if that is any different or not.
I don't think there is. But I may have missed your intent. The question was posed to ensure that we are on the same page. If you leave it up to me, we are on the same page, which means your earlier comment really doesn't work. There is no `if err != nil` to be found.
> But: with a stacktrace and a good language, this is a single line of code.
Why can't it be a single line of code in Go? In fact, at one point Go even did include the stack trace in that single line of code in some pre-release work, but real-world usage determined that nobody ever used it (all the information you need is already there without a stack trace!), so it was stricken before final delivery. You can still do it yourself if you want, though. Errors are not magic.
Because Golang has (to my knowledge) no support of any syntax that supports that.
You are a bit hard to discuss with, but I want to show good will, so I'll try to explain and hope you can appreciate that! :-)
Golang (just like most, but not all!) languages has one default way of doing things. Which is: execute each line (or statement / expression) sequentially.
That's why you can write `loadMissiles(); fireMissles()` and it works.
But it could be different. Imagine a language where each of those is, by default, executed in parallel. There are academic languages that actually work like that.
How would you then do something sequentually? By rewriting your code: `var result = loadMissiles(); fireMissles(result)`. This is a semantical enforcement of sequential execution.
Now let's change this a little bit and add a `.then()` method onto every value (even `null` if the language has that). Then we rewrite the code:
`loadMissiles().then(result -> fireMissles(result))`.
Looks familiar? If we add builtin error-handling then we just have re-invented javascript promises and this is not a coincidence.
Now, there is a duality to that - executing code independent of each other, so non-sequential. (whether it is actually run in parallel or not does not matter, as long as the outcome is the same, minus performance implications of course).
How would one do that? By adding a new method, let's call it `all()` that accepts a list of expressions. Unlike methods like .fold or .reduce, there is no way for the elements inside the list of expressions to interact with each other. That means even in a language that is "sequential by default" these expressions can (or could) be executed in parallel without a problem. This is basically Promise.all() in javascript.
Two more final steps.
First step: we have now invented promises (including sequential and non-sequential execution) which describe asynchronous computations. But how about other things? Let's think of results. They are similar - sometimes we need a successful result to continue (sequential) sometimes we can execute logic non-sequential. How about optionality? Well, it's basically like a result where the error has no information, so same thing. How about parsers? Sometimes we can need to parse something and then we decide how to keep parsing based on the result (sequential) - sometimes we can parse multiple things non-sequential. What about resources? Sometimes we need a database connection to open a network connection (sequential). Sometimes we can do both non-sequential.
And so on. See the pattern? Let's call those things "contexts" and then allow developers to define those contexts themselves, because we certainly can't foresee all contexts that exist in the world. Certain things are necessary to allow to do that, including some kind of parametrism (like generics).
Second step:
Now that we have those contexts, we can use them. But it would be nice to write code in the same way as "normal context" code (whatever that means for our language). So we should have some syntax to help with context switches - optimally for both sequentual and non-sequential logic. And optimally generalized and not specialized to single contexts.
Different languages have different strategies for the second step. Golang doesn't have anything like that (well, to my knowledge, I'm not a golang dev). It certainly doesn't have a generalized version though, that is for sure.
Therefore to come back to:
> Why can't it be a single line of code in Go?
The answer is, because it lacks the syntax in the second step and - to my knowledge - the way to define contexts (at least typesafe ones, my unsafe ones are possible) and in particular the syntax to deal with them (without having to call .then() or - worse - if/else).
The single line was already demonstrated...
> That's why you can write `loadMissiles(); fireMissles()` and it works.
Maybe.
func loadMissles() {
go func() {
// Do the things.
}()
}
Maybe not.Get back to us when you gain at least a surface understanding of how computers work.
> See the pattern?
All that just to convert one type/value to another? That is complete and utter insanity.
Did you write this piece before reading the thread and decide to arbitrarily dump it upon us, totally oblivious to what is happening around you, to satisfy your sunk cost fallacy pangs?