Error Handling in Go (2016)
innoq.com
innoq.com
I've thought about this problem too (error handling in general), and with that hint of this being a "chain", arrived at this solution which I think is extremely concise and elegant:
if((err = func1()) ||
(err = func2()) ||
(err = func3()) ||
(err = func4()) ||
...
) {
/* handle the error */
}
or if you want to get more fancy and know which step failed, int step = 0;
if((step++, err = func1()) ||
(step++, err = func2()) ||
(step++, err = func3()) ||
(step++, err = func4())
) {
...
Of course this can be modified suitably if your error vs. success values are different, but the structure is the same: a chain of actions one after another, broken only by short-circuit evaluation. It's surprising that this pattern isn't seen more, because I've shown this to a few others and the response has almost always been an initial puzzlement followed by "wow, that's extremely neat" or "why didn't I think of that?" This is certainly an example of "use the language".If you want to do something with the intermediate results (e.g, the functions return more than just error) this will not be possible..
resourceName, err := connection.getName()
if err != nil {
// handle
}
fst := strings.Split(resourceName, ".")[0]
sendTime, err := connection.findSendtime(fst)
if err != nil {
// handle
}
// do something with sendtime and call new func.. you get the point
EDIT: Fix some mistakes in my example code. Obviously fake example though :) func (r *Resource) get(w http.ResponseWriter, r *http.Request){
user, err := r.authenticate(req)
if err != nil {
r.WriteJSONErr(w, err)
return
}
var records []*User
if err := r.db.Select("select id, name, created_date from users", &records); err != nil {
r.WriteJSONErr(w, err)
return
}
r.WriteJSON(w, records)
}
You would have to reformat all your function signatures to receive maybe a pointer to populate and return only an error, which could work but meh. (f = fopen(filename, "rb")) &&
...
(ptr = malloc(size)) &&
fread(ptr, 1, size, f) == size &&
...https://github.com/blockassets/bam_agent/blob/master/tool/ht...
I would like to counter with my C++ way of doing error handling without exceptions:
if (!func1()) return false;
if (!func2()) return false;
if (!func3())
{
err = get_error();
...
}
if (!func4()) return false;
Each function sets a global exception-like error object which can be
inspected by the caller if the function signaled an error (like
returning false in this example):Of course you need thread local variables for this. Not sure if this is a problem for Go or in general...
Hope you're proud of yourself, lol.
Rob Pike's example code is bad and I would not allow it through a code review. Why? It makes the code lie. It appears as though the writes cannot fail, but in fact they can, and later writes become no-ops. On top of that, it means that you lose the context of which write failed. Did you fail writing the header, the body, or the footer? You can't know anymore.
This is like
try {
doThing()
doThing2()
doThing3()
catch (Exception ex) {
// handle
}
You can't tell what failed. You can't tell what is possible to fail. The handler can't differentiate.>Did you fail writing the header, the body, or the footer? You can't know anymore.
You are writing to the same stream in both cases. The error wouldn't be caused by the header or footer data, but by some property of the stream, like it being closed, or the disk running out of space. Knowing whether the failure occurred while you were writing to the header or footer is unnecessary information, and therefore obfuscatory.
"The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." - Edsger Djikstra
Or said another way, a good abstraction automates a precise set of tasks instead of adding ambiguity.
Maybe we're just trying to say the same thing here.
So are you making a general case for exceptions or are you just asserting that there exist cases where we don't care where errors happened? There are absolutely such cases. When writing quick and dirty scripts I absolutely value that they crash when they couldn't find a file or similar.
But for more complex systems, I've never found a good use for abstractions. In a complex system, you need to see what happens. Out of sight, out of mind. There is no way building a complex system on top of exceptions and having a good understanding over its failure modes.
I would go further and state that it's extremely uncommon to be able to handle an exception in an appropriate way. The most common use of them is to either 1) handle the error immediately (for example FileNotFound), in which case the exception handling adds just unnecessary syntactical variety. Or 2) just let the program die. Coincidentally, golang has that too.
There may be rare situations where you can handle a series of statements with a single exception handler, like several write-file statements in a row or such, and that might seem cleaner than repeating the error handling. But actually these cases are just a code smell. You could rewrite them using a for loop and factor the repetitions as actual data, adding the error handling only once (inside or after the for loop). In other words, exception handling is just a way to paper over that code smell. It makes hiding bad code easier, so leads to bad code.
You won't find hundreds of purported simple/easy things that are exclusive to Go code.
There's probably less than 10 cases where I implemented some simple thing (like a set of strings) that Java gave for free because of library function that used generics.
Among 50k loc that's background noise.
from README:
// ... use session session.Close()
can be changed to this right after you get a session:
defer session.Close()
This is precisely how I feel, could never capture it in words myself.
Sounds like JavaScript/Node all over again, which shouldn't surprise anybody as many of the prominent Node developers hopped over to Go.
a = append(a, el)
pop: n := len(a)
el := a[n-1]
a = a[:n-1]
It took me, literally, 10 seconds to write.I looked it up and it's not but the point is that you shouldn't have to worry about these things, including implementation details of trees and hundreds of other data structures.
I don’t think I will ever understand why so many go advocates seem so openly hostile to encapsulation. If go had used C-style for loops, I swear there would be people here defending the decision saying:
sum := 0
for (i := 0; i <= len(a); i++) {
sum = sum + a[1]
}
“See? Only four lines. What’s the big deal? It took me literally thirty seconds to write.”Compared to
array.sum()
was the above code more or less clear? Is it more or less prone to bugs? When reading, which code will you have a quicker understanding of the intent?And if you did notice the the first bug, did you notice the second bug?
This experience makes me suspect that there will always be some pain point with the language. We'll never be happy; that's impossible. The only thing we can do is choose what type of unhappiness we are willing to live with.
The thing I love about Go is its fundamental clarity. It's very upfront and literal. I find it easy to understand what is happening in any particular bit of code. And I suspect that whatever complexities we add would compromise this clarity in the name of brevity. I'd spend less time being bored, and more time being puzzled or incredulous. And fundamentally, I'd rather be bored.
Won't dispute that but did that materially affect your chosen paradigms and patterns?
If you compared modern C# to C++03, then maybe (although even then I would argue that templates alone are more complicated still).
Not that I ever had this sentiment but even if so: why would you try to do that? Most new features are tiny enhancements you can live without and the big changes (generics, Linq) you cannot do without so you they are forced anyway. ‘Trouble’ sounds like you were actually bothered by it which seems a bit over the top?
The only problem I have with it is it is still really Windows only, hopefully that will improve with .Net Core 3.
Maybe you are talking WPF / Desktop only; there are other options for Mono but yes, there you would be right. However the trend is, unfortunately, toward browser interfaces (Electron etc) and those you can do on Linux/Mac already with .NET Core.
I wasn't thinking of Desktop although I would love to see something strong emerge there (other than Avalon).
Ah, curious what made you feel that. We ported massive code bases of ASP.NET and commandline tooling over to .NET Core since the 1 and had not many issues. It's a much better experience now but it never felt unfinished to me.
I have to admit that I have been writing software for a long time and one of the things I automatically do is abstract (not too far, just far enough) the underlying implementation of whatever I make/made. So our old ASP.NET code was very easy to port for that reason; I never use internals directly and still do not. For instance MVC looks more or less the same anywhere so I just use plain old C# classes as controllers so they can be reused by apps, other (non ASP.NET) frameworks, commandline, tests etc. It adds a thin layer below them so they work but it saves a lot of time and with the coming of .NET Core it proved smart once again.
I'm developing a C# application on Windows (netcore 2.1) and it deploys and runs on OSX without a glitch.
I've been writing games in C# that run on Windows, Linux, OSX, iOS, Android, Windows Phone, UWP, AppleTV, Nintendo Switch and more for years now.
Ironically, the most compatibility issues we had was with Windows Phone and UWP.
I really appreciate what I've seen of both Rust and Go myself. They both feel more approachable than the C/C++ legacies imho. Though I haven't gone much farther than general tutorials with either.
When reading Go codebase I'm not familiar with, I'm very often having a hard time figuring which interfaces go with which structs. As in "Oh, this function accepts interface Foo, which is implemented by which structures?" and then I have go on a adventure throughout the codebase to figure out what structures go in there. Really annoying.
In a language whose intention is to be explicit and easy to read (as opposed to write) I can't understand for the life of me why the authors chose to make interface implementations implicit. It seems like a decision that pretty much only has downsides and which is contrary to the overall aim. I just don't get it.
Suppose Bob's codebase has a structure implA that implements interface A, and you want to use in terms of its interface. If the language requires Bob to declare implA as an implementation of A, you have to persuade him to do so in his codebase. But if implicit implementation is enough, then you can just use implA directly as an A with no changes by Bob.
But yes, this is a sore point. It would be convenient to be able to declare structures as implementations of particular interfaces, even if the current implicit behavior were preserved.
Not necessarily, in some lanugages you can add an interface to type even if the type is foreign and you only control the interface. I believe this can be done in Swift, Rust, F# and quite likely some others...
Strangely, I feel the day Go has generics is the day its attractiveness will start to fade away.
The only way I've found to avoid RSI and make error handling just about tolerable is to use an IDE that supports code autocompletion - e.g. type `err` and hit tab and it should replace it with an `if err != nil {}` block. The other thing is to use pkg/errors[1] and wrap the error so you've actually got a stack trace if you want to log the error further up the call chain.
Error handling is a solved problem - exceptions, or like the article says monads if the language is functional. The fact that Go supports neither out-of-the-box is a frustrating waste of time.
True.
> exceptions
Nope. Exceptions aren't capable enough; you really want conditions & restarts: http://gigamonkeys.com/book/beyond-exception-handling-condit...
Conditions can be signalled, handled & resignalled like exceptions, except that they can be optionally ignored and handlers can optionally choose to restart. An exception unwinds the stack, while a condition is handled prior to unwinding, which means that the available recovery strategies are more numerous.
They've been perfectly fine for the majority of popular languages and are a vast improvement over manually mimicking them the way most people do with Go.
Exceptions are an extremely useful and widely used tool for dealing cleanly with certain classes of errors (through a non-local jump out of a context to a dynamically bound target), let's call them simple errors. Maybe something that can be oversimplified like this:
def main ( ... )
try
...
catch
case e => print "Something's wrong, aborting"
(Note that in Go's original main use case (writing low-level servers), such simple error handling is rarely what's needed.) If you need restarts etc, then standard exceptions are not the right
tool. In my experience, it's worthwhile to stop thinking about errors in such
situations but see the behaviour that you are tempted to classify erroneous as part of the normal algorithm, using control constructs other than exceptions. monads if the language is functional
Monads are not a functional concept. For example an imperative language like Scala (which, being in the ML family of languages has a powerful and first-class functional sublanguage) has monads that work perfectly fine with imperative computation. Indeed, I find it a good programming style to let
trivial, ubiquitous monads, like printing / IO / global state, be handled by the built-in primitives, while higher-level, unusual, application-dependent effects are implemented explicitly using monads.Monads are essentially a form of overloading / re-defining composition operators, e.g. the semicolon operator in traditional imperative languages. In languages like Haskell and Scala, types (and higher kinds) are used for this purpose, hence it is often though that monadic programming is restricted to statically typed languages with powerful types, and don't make sense for dynamically typed languages. Nothing could be further from the truth. All we need is a 'hook' into the sequencing mechanism(s) the language provides, at least in principle. Admittedly, the outcome is not nearly as pretty as in statically typed languages. See [1, 2] for discussions.
[1] T. Garnock-Jones, Monads in Dynamically-Typed Languages. https://eighty-twenty.org/2015/01/25/monads-in-dynamically-t...
[2] O. Kiselyov, Monadic Programming in Scheme. http://okmij.org/ftp/Scheme/monad-in-Scheme.html
That is really all I have to say. The rest of this comment is designed only to prevent my being dismissed by a reader because what I wrote above is not precise enough.
When I referred above to monads in Haskell it would have been more precise for me to write "the IO monad and if you are into monad transformers, user-defined monads with the IO monad in its implementation". Monads have multiple uses in Haskell. (e.g., the List monad is involved in the implementation of list comprehensions.) It is only the IO monad (and maybe the relatively obscure ST monad, but every use of the ST monad can be transformed into a use of the IO monad with some loss of conciseness and maybe readability) that is involved in the guarantee about side effects.
When Haskell and its standard library were being carefully arranged, the designers had to make some choices as to what is considered a side effect and consequently what is in the IO type. For example, a loop does not have to have `IO` in its type signature in Haskell, so looping endlessly is not considered a side effect in Haskell even though maybe it should be, and in fact it is possible to define a language similar to Haskell where a loop must have `IO` in its type signature: specifically, you get rid of tail recursion with the result that the only way to write a loop is to use the fixed-point operator (i.e., the Y combinator, which since you got rid of tail recursion you have to provide as a primitive of the language) and then instead of giving Y the type (a -> a) -> a you give it the type (IO a -> IO a) -> IO a. There is some ambiguousness in other words in what is considered a side effect, but that ambiguity does not undermine my point because any skilled definition of side effect in Haskell will provide some guarantee.
I agree monads can be understood in multiple ways. But so can side-effects -- as you point out yourself ("designers had to make some choices as to what is considered a side effect"). Why should Haskell's definition of side-effect be the be-all-and-end-all of analysis on what counts as an effect? I do think that Haskell's choice of what counts as an effect is wrong, both from a theoretical POV and pragmatically. I'm happy to defend this claim, but HN is probably not the right venue for this. (Just two hints: (α) if you think about types from the POV of guaranteeing linearity by types, then non-termination is clearly an effect, and not one that should be IO; (β) Scala's approach to monads doesn't guarantee effect freedom, yet is widely used.)
I consider the (co-)monad abstraction to be orthogonal to effect definitions, with the purpose of (co-)monads being to allow abstracting out certain boilerplate do to with composition.
I've drafted a menu of possible requirements for Go 2 error handling[2] because the Go team's Draft Design document omitted a requirements section.
[1] https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback
[2] https://gist.github.com/networkimprov/961c9caa2631ad3b95413f...
some of the creators i have followed for years because on their take on succinctness and simplicity in the software realm. and there we are with Go becoming somewhat verbose to write in my opinion. as opposed to C or Python, or even Rust, if you will, for example...
There are, fortunately or unfortunately, a lot of very good counter-arguments and alternate proposals and specific implementation debates. So I'm not sure who long it'll take, and as much as I Want It Now™ I do think it's important to be careful for the long-term health of the language.
[1]: https://go.googlesource.com/proposal/+/master/design/go2draf... , or https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback for the current state / counterpoints / etc