Yep, I also think that's a good way to think about it. But I don't think implementing that one or two times for an entire program is too bad if it forces you to think error handling into your design.
Maybe and Either constantly show up when people discuss patterns to make error handling less clumsy in languages without exceptions baked in (and in asynchronous contexts in some languages with exceptions) - I just wish posts like this named what they're talking about better - Rob Pike is undoubtedly aware of the names.
Oh, I dunno, maybe the fact that he designed a programming language which practically proves he must have interest in programming languages. Combine that with the fact that Maybe/Options are neither new nor unusual in language design and fairly heavily discussed and I think it would be practically impossible for him to not have known about them a decade ago. Even failing at that, he would doubtlessly have heard of them since because people discuss them in relation to Go so often.
So yeah, that's how he would know about them. They're extremely common in two areas of his interest (Go discussion/criticisms, language design).
Given that the party line on generics in Go has been "Go doesn't have generics, but since generics are mainly used to implement generic collections, you can just use the built-in collection types for those purposes", I wouldn't be shocked if the designers of the language only had a minimal or cursory understanding of Maybe, Either, and friends. I do not mean this as a dig or an insult, but if Rob Pike is at all familiar with the benefits of Maybe and Either (or sum types in general), then it does not show either in his work or in the talks he's given.
A generic maybe/result/either? Yes. However the maybe pattern is built into the NullString, NullFloat64, and NullInt64 types in the database/sql package.
That defeats the point of having a type system (casting is as error prone as forgetting to check errors).
Also it doesn't allow for the use of already defined functions to do the transformations, (e.g. `maybe.flatMap(existingFn).flatMap(otherFn)` wont ever work, new closures will have to be defined that do casts before calling the existing functions).
So basically, its just-as-error-prone and just-as-tedious as checking error codes but in a different way. Both benefits of the pattern are gone. Given this, I don't think that the word "impossible" is a stretch at all (not having any of the benefits of the pattern = not implementing the pattern)
Maybe is not an error-handling mechanism because it doesn't report any error message. I don't know why Haskell programmers think it's any good for reporting errors.
You could use Either, but it doesn't give you any guarantee that you'll get a string back, and the naming is lousy. (Left and Right? Really? I know there's a mnemomic, but we can do better than that.)
So, might as write your own ADT and name it right.
(Also, a real error-reporting mechanism should support cause chaining so you can see the dependencies; you can sort of do it with string concatenation but it gets awkward after a few levels.)
> You could use Either, but it doesn't give you any guarantee that you'll get a string back
1. It's better not to use strings and instead use a user defined error type.
2. What do you mean "doesn't give you any guarantee you'll get a string back"?
> real error-reporting mechanism should support cause chaining so you can see the dependencies;
Can you elaborate on cause chaining, perhaps with an example?
What I mean is something like a stack trace except without all the extra detail:
- Can't persist shopping cart
- RPC request failed: http://example.com/update_cart
- Can't write to database "carts"
- Can't save file: "/a/b/c"
- OS error: disk full
Ideally, all errors in the cause chain should be logged, and they typically cross abstraction boundaries; the message at each level comes from a different part of the stack. This seems hard to do across languages, but within a language there should be a standard way to do it.
(Go falls down on this too. Perhaps we don't need Java-style stack traces that go on for multiple screens, but we should have more than a string.)
Maybe is just an example. If you need to return more error codes, perhaps with contextual information such as message(s), then just define your own discriminated union and use that.
> (Also, a real error-reporting mechanism should support cause chaining so you can see the dependencies; you can sort of do it with string concatenation but it gets awkward after a few levels.)
>We recently scanned all the open source projects we could find and discovered that this snippet occurs only once per page or two, less often than some would have you believe.
once per page? that's once per 30 or so lines. 2 or 3 percent of the code is this same repetitive structure? Also, only takes one time to forget this for everything to fall apart.
We've spent so much time trying to deal with uninit'd pointers and things like that on the language level, and here we have something that's much simpler, yet super repetitive and should be easy to catch on compile time.
Maybe I'm just a bad programmer, but I really prefer the Java "force you to catch exception" model much more to this.
>It's worth stressing that whatever the design, it's critical that the program check the errors however they are exposed.
If it's critical to check the errors, I think that it's worth writing language rules in a way to force people to write a "has error " code path (think pattern matching).
I get go is trying to be as small of a core as possible, but I think there's a lot of value in extra compiler checks (with no runtime cost!) to catch this sort of thing.
> I really prefer the Java "force you to catch exception" model much more to this.
https://github.com/kisielk/errcheck/ will tell you if you silently discard the `err` value (though you can still explicitly assign it to _ to ignore). Seems worth using for a lot of projects.
We use errcheck for all the Go code we write at Shopify. In many cases, we compile the project using a script that fails if errcheck isn't clean, effectively turning into a "you are forced to catch the exception" sort of idea.
It's nice because it's still easy to ignore the err in certain circumstances (e.g. the occasional `_ = f.Close()`), but there's always a visual marker to remind you.
The above comments are just pointing out that the compiler should do this for you. It is great that there are third party tools for this, but a simple lint like this should come for free, especially when this is the endorsed error handling policy.
Not everything belongs in the compiler. Go's "vet" tool might be the place for something like this. But, honestly, I have been writing Go code for 5 years and I can count the number of times that "didn't check the error" was the cause of a bug on one hand.
Agree on not in the compiler. A ton of folks seem to dig errcheck; having that sort of check in vet or in the Go distro somehow seems worthwhile. (Of course, if I feel that way, maybe I should put some effort where my mouth is and try and start on a patch. :)
I'm actually quite surprised this functionality hasn't made it into "go vet" yet. It would be useful to be able to do this without a 3rd party utility (although all respect to kisielk for producing such a useful tool).
> that's once per 30 or so lines. 2 or 3 percent of the code ... I really prefer the Java "force you to catch exception" model much more to this.
And that 2%-3% doesn't include where errors should have been checked but were not. For instance run programs without stdout ("./program 1>&-") and most Go console programs will just happily write to nowhere whereas almost all Java ones error out immediately. Closing stdout doesn't often matter, but sometimes things like this are important.
I don't understand how in the 2010s it's okay to make a language with worse reliability. And worse "JavaDoc" equivalent. Go should be better than C and Java in every category and it's just not better in way too many.
While your comment is sort of true, it's not applicable here really.
We know how to handle errors better than this. Go's error handling is C style. We've since found exceptions, Maybes, and friends which are very powerful.
What you said implies that when Rust chooses to use Maybes they're somehow being bad at something. That's simply false; computer science as a field has advanced and one of these advancements (not even that recent of one) is improved understandings of how strong type systems can be used to make code, including errors, safer. Go completely ignores this of course and they're not doing it for a tradeoff that I can see. They gain nothing in terms of performance for not having this type (everything is already a reflect/Value anyways, so much for not having the baggage that you'd need for generics) and everything in terms of having a worse language.
Being good at something doesn't instantly mean you're bad at something else. In fact, I wouldn't even claim that's the general case.
If you disagree, you're welcome to point out the tradeoffs at work here. I think your comment is just a vaguery that is completely untrue in this instance.
I think this argument is depressingly uncharitable.
One of Rust's goals was to remove the "feature" of null pointers. In its place rose the feature of tagged unions, to support the `Option` type. This is strictly trading one feature for another. Once this necessary task was completed, Rust was happily able to reap all the benefits of tagged unions elsewhere, such as with the `Result` type. It is not greater language complexity, it is merely the reuse of simple language features to empower developers to create more robust software.
Now, I am not among those saying that Go needs tagged unions, or any other feature for that matter (no, not even that one). I think Go is a good language, sometimes to the surprise of my peers. But what's disheartening to me is seeing a Go core developer imply that Go is literally the baseline for programming language simplicity, in effect papering over all the various tradeoffs that programming language design entails.
You mischaracterised my point. Go is hardly the baseline for language simplicity. We just made a different set of trade offs to the Rust team, and I believe Go is the simpler (not simplest) language.
>> We've since found exceptions, Maybes, and friends which are very powerful.
I think, and someone correct me if this is wrong, that implementing exceptions in Go is prohibited by go routines.
The stereotypical way to implement exceptions is using setjmp/longjmp which unwind the stack back to a known location. Since go routines are implemented as continuations, any particular executing go routine may or may not have it's stack in context, and I'm not sure what it means to unwind stacks across multiple continuations, ie is it even possible?
The tradeoff is complexity. You need to define a maybe for your return value, which needs generics. Whereas Go gets 80% of the functionality with multiple returns which are like 10% odd the complexity (and are useful in other ways).
You don't have to have generics to use enums for error handling any more than you need generics to use tuples for error handling. The thing you need is enums/algebraic data types/sum types.
My go code is way more reliable than my exception-language code, because I know exactly where it can fail and what happens when it does. It is much harder to see where exceptions can happen and what the code paths will be when errors happen.
Why is not having stdout automatically an error condition? Lots of programs should work fine with no stdout. If a program requires stdout and doesn't fall when it's missing, that's a bug (and pretty easy to fix).
> Why is not having stdout automatically an error condition?
In UNIX the file handle an opened file gets is the next available, lowest number. Closing stdout means that the program opens a file and it gets file handle 1 instead of a higher number. Then it writes something to stdout, which is an alias for file handle 1, but instead it writes to the opened file.
I believe this caused some security bugs in passwd or maybe su where you could run it without stderr and "Password:" would be written to the passwd file instead of stderr. ...but these specifics are beside the point, which is that it is next to impossible to anticipate the problems an unhandled error can create.
Java's checked exceptions can be annoying when you have to add some extra "throws X" or handle every case, but my experience is that I never had the slightest doubt over how a method could fail or what needed to be done to handle errors. For unchecked exceptions maybe you have a point, although their failure mode for unhandled errors is still to abort the program rather than to continue on with undefined behavior as in Go.
Go's error handling is only a problem because of other missing features...algebraic types and/or generics. With either of those features, you could implement two-track, functional composition with a single bind method. That would go a long way towards only having to write your "if err != nil" once rather than once per function invocation.
Alas, the lack of those other features leads to articles like this one that offer unnatural ways of avoiding something that is pretty much idiomatic in the language.
I'm a Go developer, but I don't buy this. At best it's showing how, with a bit of work (often times by the developer), errors aren't that bad. But I don't see how it's showing that it's better?
If you log the error when checking if ew.err != nil, which might be many lines or even a different function, you'll have a hard time getting a meaningful context (which is hard to do in Go in the best case), like a stack.
One way I could see to make this more generic is to provide the errWriter strict with a function to call on the error if it's not nil, but i'm not familiar enough with Go to know how we'll thug might work.
The point is not to use this exact pattern, but to write code to handle errors in a way that works in your context. If the error check is many lines away or in a different function, you should use a different approach.
In both cases an error value is returned to the package user.
In general, I think you should feel freer do abnormal things that suit your situation within a package as opposed to across API boundaries. The standard ways of doing things should still be preferred when it's not too costly: the standards are what they are for a reason (even if the tradeoffs they represent are more appealing in some situations than others), it's easier to read code that's written in the way everyone's used to, and standards help keep you from spending time bikeshedding unimportant details. But the surface area of your package is the first thing users will have to learn, and what you have to keep in your head whenever maintaining code that uses your package--that's what it's most important to make clean and normal-looking and consistent.
I know the post talks about using an unusual error-handling pattern in an API, and you can do that sometimes, too. Just trying to make a separate point--when you're thinking about doing unusual stuff, keep in mind there's a big difference between an unusual implementation and an unusual API.
Panics can be incredibly useful. I use them in HTTP services to simplify returning error JSON objects to the consumer.
In general I'm a bit surprised about all the hate(I say hate because of how people react to it and react to suggestions for how to better work with it) the error system gets. I find that creative use of error/panic can usually scratch my "this is too much error handling code" itch when it flares up. As long as it's documented in the API I agree that unique public error handling strategies are not all bad.
Interestingly I write a lot of C#, which of course has try/catch/finally, and I find that if you are doing it properly you have many levels of handling and a strategy involved at the different levels for what should done when. I find that many people write TERRIBLE error handling. There may be try/catch sprinkled around but there will be no strategy to it with edge-cases and improper handling abound(masked exceptions, finaly's throwing new, unrelated exceptions, etc). I personally find the amount of effort required into developing and executing a robust error handling strategy to be about the same between C# and Go. That's not to say that I don't sometimes wish Go had C#'s error handling( ;) ), but I realize they are different beasts with different masters.
How about just using beloved tables instead? Maybe even with function literal to prepare some stuff, if you need to.
for _, s := []string {
p0[a:b],
p0[c:d],
// ...
} {
_, err = fd.Write(s)
if err != nil {
return err
}
}
As others already implied it could have been cleaner with exceptions, but I don't think this is that one case where exceptions are particularly important. There is no recursion yet or anything like that.
One thing I love about the repetitive err pattern is that I find my self wrapping/appending errors with useful context more frequently (rather than say catching an exception five frames further down and just appending "mylib: oops").
I realise the later was just laziness on my part, but the constant err checking really does force good habits on me, which of course yields higher quality code that's easier to reason about when something goes wrong.
> (rather than say catching an exception five frames further down and just appending "mylib: oops").
That is why I like exceptions, so I don't have to explain possible errors like this if I don't have to, but can get a top level exception and see a stacktrace with it. Then choose to add aditional info.
I really like that pattern and use it quite a bit. For example, it is easy to turn code that connect into a piece of code that re-connects with a back-off without having to modify lower layers of the connect call itself (say because it is already in a library).
Errors are absolutely values and that's a great perspective for any language to take. Essentially, you can see an errorful computation as "returning" either a valid return value as expected or an error. In Go the quotes are eliminated: that is exactly what happens
ret, err = fd.Write(...)
Above we have two (initial) possibilities
1. The return was error-free, err = nil, and
ret is an interesting return value.
2. The return was errorful, err != nil, err
instead details the reason for error as
an integer, and ret is nonsense.
This is a little challenging because we must be sure to check err in the second case since otherwise ret is silently meaningless. This is a source of bugs called "Boolean Blindness".
In a language with Sum types we'd represent this by saying that errorful computations adjoin an error type to the return type.
Int ----> Either Error Int
What's nice is that this automatically ensures the above invariant of err/ret is held: a value of type Either Error Int is either "Right i" for some valid Int i or "Left e" for some error value e. The first thing we check is the handedness of the result and thus know the result of the computation.
Sum types like Either solve Boolean Blindness because we literally cannot get access to our return type Int unless it is valid: if we find that the result is "Left e" then all we have is an Error value.
(Ultimately what this boils down to is that product types like (ret, err) behave very differently from sum types and cannot replace them.)
If Go had generics we would also notice that the error-passing behavior of Either is naturally ignorant of the particular choice of types Error and ain't. We could manipulate it meaningfully (and indeed write the functionality used in this article) for any and all values e and r as the type Either e r. Then upon use Either e r would specialize to Either Error Int.
It's easy to argue that this is silly: we only need one error type and it is not difficult to replicate this code. I'll argue for now that only having one error type is at best confusing and at worst a lie. Different processes and functions have different exceptional states. Coercing all of this variation in error meaning into the same error type ensures that misunderstandings and accidental unifications will occur.
Honestly, I really love Go for making sure that errors were values: it is an incredible step forward for making exceptional cases more reasonable. Unfortunately, Go does not seem expressive enough to build the machinery which makes this style of error handling all of reasonable, non-repetitive, and non-confusing.
That second-last example with errWriter destroys safety AFAICT, since it requires the user to remember to check for ew.err to see if the whole sequence of operations actually succeeded or not.
Atleast the last example with flush returning the error made sense, since that's an explicit "commit" operation that the user must call for correctness. However not every series of operations has a final commit operation, so this technique is unsafe in general.
(In fact even flush might be optional? I didn't check the docs.)
> That second-last example with errWriter destroys safety AFAICT
It is possible to make mistakes. Go isn't the kind of language that forces you to be safe and do the right thing all the time. Like all languages, it makes tradeoffs.
I get being against exceptions in a language like C++ where it can leave your program in a really weird state and have some ugly performance implications, but I don't get the argument in languages where it can work just fine.
The argument seems to be "you should be checking for errors anyway", but I feel like there are basically two types of errors: the obvious kind that you're going to check for anyway (or get bug reports), like network disconnects and bad files, and the "how the hell did that even happen" exception. For the obvious exceptions it seems like a wash, other than the try catch block being a bit cleaner, but for the "hell the hell did that happen" error exceptions seem way more useful. I'd much rather my program explode immediately than risk someone letting the error passing silently because people didn't check err because like "that will never happen". I know, functions can call panic and all that, but it just seems clumsy for no net gain.
I do think there's one type of error checking that doesn't get enough love (I don't know if Go has this, but I'm guessing probably?). "assert" that gets removed in release builds. It's kind of silly when you're working alone, but in a team of programmers or for libraries it's awesome, because it's basically like runtime documentation that gets compiled out. So you can warn your fellow devs about things without it bloating your program. The newer C++ has a really great feature called "static_assert", which is basically a compile time assertion, which can be handy for things like checking in various places that maybe if the count in enum changed, that you accounted for it in another spot of the code, or that certain aspects of a new platform sit right, or things like that.
> I get being against exceptions in a language like C++ where it can leave your program in a really weird state
If you're programming in modern C++, then exceptions allow you cleanly (as in, clean code) leave your program in a very well defined state. You have to adhere to RAII, and there's a lot of C++ shops out there that don't allow RAII (and are wary of exceptions, because they ignore RAII). I've routinely used exceptions in C++ as a both efficient and clean way to propagate errors out to a stack frame that can handle them, fully confident that RAII will clean up as I go.
> and have some ugly performance implications
More than having an error object that the calling frame needs to check? If I understand Go (and, since it's mentioned here and I think is relevant, Rust's) setup, each stack frame between the frame that can handle the error and the one where the error occurred must check that an error did indeed occur, and if so return it. Rust makes this pretty through macros, and the article has some good thoughts on it for Go. Nonetheless, can the compiler compile that to better than an `if error { return error }` type check?
My understand of C++ exceptions is that this was basically how things _were_ done (but no longer). Previously, exceptions were managed by "sjlj" (short for setjmp, longjmp, the calls used to manage exceptions); any frame that _could_ have an exception would need to call setjmp on entry, and this was a bit hefty.
However, my understanding is that, at least for g++ on Linux, C++ has abandoned this in favor of a newer scheme called "dwarf2"[1]; this scheme is such that entering a "try" (or a stack frame where RAII would need to do actual cleanup) is free (rather, it is covered as part of the normal stack setup, really), leaving it gracefully is also free; only during actual stack unwinding do exceptions add code/performance penalties.
I think there is an unfortunate amount of information that C++ exceptions are inefficient left over from the sjlj days.
Entering a try with newer implementations of exceptions is free in that it doesn't execute anything extra, but it's not free. It inhibits the optimizer and creates a lot of duplicated code in the binary, as well as forcing exception safety on the programmer, which is pretty hard to achieve completely, efficiently, and/or cleanly.
Yeah, I mean, that can work, but I feel like the phrase "in modern C++" always has the hidden caveat of "Unless you're stuck with a bunch of legacy junk". A lot of times you end up having to use legacy code you can't trust. Your stuff is RAII, but this old legacy code you have to link with... and nobody wants to rewrite it... you probably get the point.
I'm not against C++ exceptions, I just think there are legit reasons for shops to avoid it in that particular language . But I like exceptions in general.
>I'd much rather my program explode immediately than risk someone letting the error passing silently because people didn't check err because like "that will never happen".
Note that with java's checked exceptions, we get the exact opposite effect: lazy programmers catching every exception and either doing nothing at all with it or at best, printing the stacktrace and moving on. And it doesn't matter if you're not lazy yourself, the third-party code you reuse will do it.
Then there are exceptions that shouldn't even be checked, for example, you call UrlDecode and specify "UTF-8", gotta catch an exception in case "UTF-8" isn't a valid encoding name!
>try catch block being a bit cleaner
Except when you want to declare a variable and initiate it in the same line, but can't because then it would be scoped to the try block
Of the various approaches I've seen to cope with this problem, Rust's try! macro is the most elegant.
Given the practical minded nature of Go and the core team's willingness to add in special cases to the language where appropriate, I think a Go version of try!() would be welcome and fit with the ethos of Go. It solves the 80% case and it doesn't mean the creation of a type-safe macro system.
The thesis of the blog post is that errors are values and thus you can program around the repetitive handling of them. But the proposed solution of writing a one-off implementation of Maybe like `errWriter` isn't an answer because:
1) Most importantly, when you need to call 3 functions which return differently typed values and can fail, there's nothing in the language to help you. This is the common case.
2) The implementation is now less obvious. To understand it, I need to read the `errWriter` helper code.
3) The code space you'll save by writing a new Maybe type is negligible.
try! isn't too hard to emulate in Go, simply write a wrapper that panic()'s on and error and passes the first value through.
The rust Result idiom is easy to implement as well, and is used frequently in the database.sql package to handle nulls.
In this way, rust and Go are not that different. The biggest difference is the lack of a generic Result, which means you have a NullString struct with a Valid and String method.
The thing is, without generics, you need to write a new wrapper for each type you're dealing. Add to that that this really isn't idiomatic Go style (code that panics all the time is frowned upon), and most people don't bother.
This is a pretty good illustration of both the point in the article and why some folks don't like Go. It's programming with error values; you can decide exactly what you want to do with the error, including panicking when you encounter one, and use the language's own abstraction mechanisms to write that code. OTOH, the reason why we have exceptions in most mainstream languages is because many, many C programmers wrote the same error-handling code over and over again, and eventually someone decided that it ought to be a language feature. The point of a programming language is to abstract out common patterns that you'd otherwise have to write repetitively.
What in Go is a return type of (Anything, error) with typically exactly one of the two being nil is in Rust the Result type, `enum Result<T, E> { Ok(T), Err(E) }`, something which is explicitly exclusively-either an OK T or an error E.
The equivalent of `x, err = expr; if err != nil { return err }` is very common in Rust; it would be written `x = match expr { Ok(x) => x, Err(e) => return Err(e) }`. It’s such a common pattern that a macro named try! was placed in the standard library for this incantation. Therefore you can write `x = try!(expr);` (or if you’re going to ignore the Ok variant, just `try!(expr)`).
Rust also warns you if you do nothing with a Result object; you can ignore the warning or suppress it, but it all makes it very much harder to ignore such error conditions. Go is one step up from C, where integral error-indicating return codes are the norm and are typically ignored, but I feel it’s still a fair way off the confidence that Rust gives me that I haven’t accidentally forgotten to deal with an error case. If you want people to get the message “Whatever you do, always check your errors!”, it helps if you design the language to make it hard for the user to forget.
(You people that know Rust, I’m ignoring the `FromError` part for simplicity. That’s only in libstd’s try!, anyway; libcore’s try! doesn’t have it.)
(And with HKTs you can abstract this whole blog post into a fully generic one-liner applicable to basically any notion of error! I can't wait til Rust picks those up.)
It does not. It's entirely speculation at this point, though many, many of us want to have it. Everything has been so focused on getting 1.0 out the door, and HKT should be backwards compatible. Special pains have been taken to consider HKT when designing APIs.
In Go I not infrequently make use of a non-nil value AND a non-nil error. A canonical example of this is the io.Reader interface in Go's standard library [1]. I think it is a very useful idiom particularly when dealing with things where failure is more normal - e.g. dealing with network services, disk IO, decoding partially corrupt exif data from images. Often you want a best-effort at the value even if you run into problems.
That can be done by either returning a tuple or providing a way to get a partial value out of your error type. (Result is generic over the error type, so you can add whatever extra data you'd like to your errors without forcing your callers to downcast.)
> In Go I not infrequently make use of a non-nil value AND a non-nil error.
The number of APIs making use of that pattern is very much infrequent, and doesn't justify it for the 99.99% of functions which don't.
> I think it is a very useful idiom particularly when dealing with things where failure is more normal
And you could have the exact same information by returning, say, `Either (Int, Error) Int`. Or even `Either (Int, Error) ()` since partial writes are apparently considered error cases.
Although I would really use a separate data type e.g.
data WriteResult = Complete | Partial Int Cause | Error Cause
and let the application decide whether a partial write is an error or normal behaviour.
As an aside, the Go solution provided in the post has the following curious property.
If you have to call `write()` 10k times (say, in a loop), and get an error on the first write, instead of either unwinding (in C++) or returning (in Rust), that Go solution requires your program to check for nil 99,999 times.
To be fair, the Rust approach is mostly the same as the Go one on the machine, just harder to screw-up: the discriminant of the enum has to be checked to be able to extract the value (or extract the error) which is similar to checking `err` for `nil`.
True, but I fail to see why it's relevant. Every mention of generic here (at time of reading) is along the lines of "this pattern could be generalised if we had generics", which IMO is a fair point to make when discussing ways to make a repetitive design pattern less painful.
I don't know if I'm just ignorant, but I like that Go forces me to use real estate for dealing with errors. It's the first language I've used where I give as much attention and design to the "sad paths" as I do to the "happy path". And I'm very comfortable navigating through text files and folding blocks of code, so it doesn't bother me if handling an error costs me a few lines.
Macros like try! might take up less space, but is that really a bottleneck for your work?
Personally? Line noise sucks. Constant error checking is vertical line noise, which is even worse; it distracts me from important things like "is my business logic correct" with "did I type that particular error check correctly?". For me, that really sucks. (I get other people like it. That's great. For me, it's junk.)
I generally use Scala and just chain through Try[T] monads and address the sad path right at the bottom, right next to the culmination of the happy path. I always know where it is, I can't miss it, I'm not repeating code all over the place and running the risk of screwing up with part of it...to me it's just a smarter way to handle the failure case.
Personally, I don't believe error checking is line noise; it is part of your business logic. Forcing me to acknowledge and deal with error cases requires that I write more robust code, that I acknowledge the failures and errors that can occur within my logic, as they are part and parcel of that logic.
If you see only the success path as the "true code" then you are missing the whole raft of other conditions that can occur; its an idealised idea of what the code is.
This is why I like Go. Its the first language I've ever used that forces me to think pessimistically about my own code.
Scala makes the distinction clear by having the difference between "=" and "<-" (or "←"); the former for something guaranteed to succeed, the latter for a possible error. This is wonderful; it's obvious enough that you see it (and the compiler catches you if you get it wrong), so when an error happens you can immediately see which functions might be causing it and which ones can't be. But you don't have to write a bunch of identical lines; one or two characters are enough for something this common. And you can reuse the same syntax for other error-like contexts, rather than having syntax for this one piece of functionality that's special-cased in your language and no way to handle similar cases (e.g. audit logging).
I don't think you actually read my post before your fingers got going. Monadic comprehension with Try[A] is effectively an Either[A, E], where E is any exception thrown within that context. Mapping over a Try[A] returns a Try[B]; if the first Try failed, it likewise failes, becoming an Either[B, E].
I am thinking about my error case. I am explicitly acknowledging that I am doing so; heck, scalac will fail to compile if you haven't folded across both the failure case and the success case. I'm just not writing the same (stupid, wordy, potentially failure-prone) boilerplate code every time I make a method call into a library.
Apologies, this is my ignorance re: scala but I have no idea what you just described, although to these ignorant eyes it appears to be a lot more complicated to read and understand than a simple if err != nil check! however, I will go and google some of the concepts you have described to try and understand better your point
It's not complicated. Your return value can, as in Go, be an error or an object of type T; unlike Go and its stunted type system, this is a generic object and so this must be written once for all types. That return value can be functionally mapped over, just like `map` in Ruby, and will return either the original exception (the mapping function will not be called) or the result of the mapping function.
These can be chained indefinitely. At the end you have a Try object of the generic type of the final mapping function, against which you can pattern match for Success or Failure. (Failing to compare against both Success and Failure is a compiler warning. There's a flag, IIRC, to make it an error, but since my IDE is already yelling at me that I screwed up I don't in practice have this problem.)
Additionally, at any point in the chain you can call `recover`, which is similar to `map` but for the exceptional case (Try[T] -> Try[T], with Success[T] passed through) if it's a recoverable error.
All error handling--as separate from error recovery, which is rarely in practice useful--is always, always, always at the very bottom of the function, right next to the happy path. It literally is impossible to miss, and does not require copy-pasting code after every library function that can fail.
There's no reason to check 'err' every time unless your language is just straight-up too dumb to do work for you. Your time is more valuable than the computer's in all but the most extreme of cases; you should act like it.
If this is the first language you've used where you give that sort of attention to errors, you must not have used and learned from the terrible mistakes of C, which constantly returns errors which are not required to be handled, just like go.
Heck, you must not have learned from bash where every second line is "if [[ $? != 0 ]]" or you just did set -e and fail hard.
For me, the problem with Go's errors isn't even the lines they take up, but the fact that they still don't accomplish anything more than C's errors.
Since C, we've learned that you can use exceptions to force errors to be handled, or Options + matching to ensure each case is handled or extremely explicitly ignored. We've learned that errors can have contexts like call stacks attached to them.
Go ignores all of that and goes back to the C way. It doesn't provide an easy way to chain errors or force them to be handled. It doesn't provide a good idiomatic way to differentiate between the majority of errors from the stdlib because it's discouraged to have multiple error struct types; rather you're encouraged to use errors.New and make an un-differentiatable error type with no good context. It's encouraged to return an err from deep in the stack without including any additional information.
In fact, Go's even inconsistent in how it uses errors in the stdlib. For example, if you look at io, you can see the following: http://golang.org/pkg/io/#pkg-variables
They define their variables as error types which are simply strings. That means you can compare with the constants (e.g. err == io.EOF), but if you add any extra info to that err, such as callstack, you can no longer compare with it. If you look at os, you see "os.IsNotExist(err)" which tells you if an err is of a given type. If you look at net ( http://golang.org/pkg/net/#AddrError ) you can see they implemented their own Error interface implementations which you check by doing `if addrErr, ok := err.(AddrError); ok { // handle error }`.. the fact that the Go authors included three different error patterns in the stdlib already shows that the entire error process in go is poorly thought out. In addition, none of the methods allow you significant flexibility in adding your own info to the error as you pass it up without losing information.
If you don't like try! in rust, you're free to write your errors out explicitly, but if try! fits then it's good to use it.
The majority of bugs in code come from people being unable to keep a sufficient amount of the program state in their head to track program flow accurately. If half of your program flow is errors, then no wonder. There's an old principal that each function body should fit, in full, on one screen because that ensures it can be read without scrolling and reasoned about fairly easily. Error handling wasting space also wastes thought and makes your code provably harder to reason about (if you don't think that's true, look at the incidence of 'mistake checking error return values in c' vs 'mistake checking error return in haskell' vs 'forgot to either catch or annotate throwable for exception in java'. Yeah).
So what's your suggestion here? It sounds like this person feels more proficient, more on-the-ball, writing Go.
So, you call them ignorant (incredibly ignorant, at that) because they're not using a language with exceptions or pattern matching, and they're wasting they're time on "C-style" error handling and... what? They're ignorant until they use something else?
You're also drawing a weak conclusion here: if you use Go idomatically, which requires more lines on your screen for error handling, and a majority of programming errors are a result of the programmer not being able to keep a sufficient amount of program state in their head because of code not fitting neatly on the screen, then using Go leads to more bugs.
> So, you call them ignorant (incredibly ignorant, at that) because they're not using a language with exceptions or pattern matching, and they're wasting they're time on "C-style" error handling and... what? They're ignorant until they use something else?
Honestly, yes, though I would try to phrase it more constructively. If you've only seen C-style languages, it's easy to be ignorant of the world out there; it's easy to try two or three languages and think you know what's going on, when in fact the languages you know all come from a very similar lineage. Like learning English, German and Dutch, and then assuming you know the general principles of human grammar.
If you've tried a language pattern matching and higher kinded types, and found them to be not useful, then fair enough. But you owe it to yourself to try, and even if you end up not using the feature, understanding it will make you a better programmer.
> Do you have anything that substantiates that?
Over many years of research into what causes bugs, there's only one thing we've ever been able to correlate reliably with number of bugs: number of lines of code. Even across different languages, the correlation holds: http://programmers.stackexchange.com/questions/185660/is-the... . So the only evidence-based way to reduce software errors is to reduce lines of code - something you can do by switching to a more expressive language.
You're right that I've never written C. I know enough bash to hate using it. I'm familiar with Maybe/pattern matching in Haskell, and I've used OCaml too. I've seen more than one approach.
I also think you're correct about everything you said, especially about the inconsistent treatment of errors in Golang's stdlib, and the difficulty of differentiating between them. I've noticed the same and it has annoyed me. When writing my own application code I do maintain a consistent pattern for returning my own errors, but I have been frustrated when consuming the stdlib for this very reason.
Maybe as my code base grows I'll come to agree with you, but for now at least it seems to be at a nice middle ground where the error handling isn't overwhelming and it isn't swept under the rug. And to my knowledge, none of the bugs I've created when writing Go code have come from my understanding of the program flow being interrupted by error handling code. And I've probably avoided many bugs because of this pessimistic approach I take when coding, as jalfresi put it in his comment[1].
Rather, (in Go) errors are values that violate type safety and destroy any correctness guarantees the compiler makes.
Languages like Rust and Haskell do this right. Both make heavy use of optionals for failable functions, and Haskell's support for HKTs (and therefore Monads) allows awesome stuff like Maybe or Either chaining.
Monads allow you to write code that, syntactically, looks like you're not doing any error handling. In reality, the error handling semantics are baked in at the Monad level. This makes sense, because error handling is usually very repetitive (e.g. "Check if the function we just called failed. If it did, return early. Otherwise, keep going." ad infinitum). As a (pseudcoded) example:
parseTimeString :: String -> Either Error Time
parseTimeString string = parse string $ do
hours <- readHours
semicolon
minutes <- readMinutes
semicolon
seconds <- readSeconds
return (Time hours minutes seconds)
If any of those steps fails, the entire computation will stop and the error message will be returned. This is roughly equivalent to checking for "nil" (in Go) after every function call, except you don't actually have to do it. This is defined in the code for the Either monad, which is at https://hackage.haskell.org/package/base-4.7.0.2/docs/src/Da... . As you can see, it's 4 lines of code.
(Side note: this is pretty much how you write parsers in Haskell using the Parsec library)
> Both make heavy use of optionals for failable functions ….
Haskell doesn't do so enough, though (I don't know Rust and so won't comment), so that we still need manually to choose to use the safe library (http://hackage.haskell.org/package/safe).
Do you know anything about the motivation of those choices? Surely it won't have escaped anyone's notice, especially not the designers', that this is a wart on the language, and that there is an easy solution; so I can only imagine that the easy solution was rejected for its inelegance—which seems strange, given the devotion to theoretical purity of the rest of the language. (I contrast the situation here to that of the numeric tower, which is clearly flawed but for which, even now, the 'right' approach is not so clear.)
Lua uses this to handle errors---I don't find it really tedious. A common pattern is kind of the same---to return either one return value and nil and then an error message or an error code (Lua, like Go, has multiple returns) in case of errors. This results in code like
let res, err = some_function()
if res == nil then -- you can also check for err ~= nil like in Go
-- return from this function with something, err contains more
-- information
end
I think it's very neat---you can also return nil, err from this function if you can't deal with it and some code up the stack should be able to handle it in a meaningful manner. I don't find it particularly tedious; i.e. not less tedious than writing try..catch blocks, and you can clearly see where are the function exit points, whereas with exceptions, any line can theoretically throw an exception. A missed error check can be troublesome, but generally using nil as a value further down in the function will hopefully trigger another error which might be then caught so you can spot the bug.
Lisp's continuations are also IMHO a great mechanism---you catch the error and then you can tell the function down the stack what it should do: you can abort, or you can try fixing the error and continuing, or you can try again for some number of times and then report a new error if that fails. It's more flexible than stuff I've heard of before.
Neither. It just basically never happens that you want both
error reporting and a maybe (nil or value) from the same function. Not in list? Nil. Could not parse? Nil. Iteration ended? Nil.
If a function be advertised to return an error code in the event of difficulties, thou shalt check for that code, yea, even though the checks triple the size of thy code and produce aches in thy typing fingers, for if thou thinkest ``it cannot happen to me'', the gods shall surely punish thee for thy arrogance.
Go lacks a construct comparable to "with" in Python, or WITH-OPEN-FILE in Lisp, to guarantee that things get closed out on scope exit. Go has "defer", but that's a clunky replacement. You have to explicitly write the "defer", and it's always function-scope, not construct-scope.
Most of the problems with exceptions in C++ come from the C++ memory model - allocation and deallocation in exceptions are usually painful, and care must be taken to clean up and unlock things. Go doesn't have that problem - it's garbage collected.
Go does have "panic", which is Go's answer to "longjmp". People keep trying to use that as an exception mechanism, which is not a good thing.
Python seems to have the best track record with exceptions. Python's usual problem with exceptions comes from library functions which, under some rare circumstance, raise an unexpected exception and take down the whole program. This is mostly a legacy problem due to the poor original design of Python's exception hierarchy. You would like, for example, for everything that can possibly go wrong with an HTTP read or a network operation to be a subclass of EnvironmentError, as caused by an external event or data. That wasn't the case originally. There are still problems in Python 2.7 with getting a ValueError because some low-level data item was bad UTF8 or something like that.
The trouble with the "errors are values" concept is that error details tend to get lost as errors propagate upward. This came up yesterday on HN in connection with network errors for GoGo's airborne networking service. I once argued that D's error type should have a "why" pointer slot, so that, when you passed an error upward, you could link the lower-level error to the higher level error. The error message printer would then list
out the errors, yielding something like
Unable to complete transaction
because database update failed
because database connection was lost
because of network error "Host down".
> Go lacks a construct comparable to "with" in Python, or WITH-OPEN-FILE in Lisp, to guarantee that things get closed out on scope exit. Go has "defer", but that's a clunky replacement. You have to explicitly write the "defer", and it's always function-scope, not construct-scope.
The problem of avoiding resource leaks is somewhat orthogonal to error handling, though. Do note that Python's "with" statement doesn't play well with "yield", which is an important gotcha. But in practice, it's quite convenient.
The handling of errors in go is weird if one only returns or breaks on errors. But it works fine if one does something on error, in almost every error check in my go code I have a log.Println with a human readable text like "could not read from the application configuration file." much nicer to get errors like that than a stack dump and a message like could not read from file $filename.
If you want to force the consumer to consider an error case and make some decision about it... Well, I actually like Java's checked exceptions.
You've got to emit an error somehow and static checking ensures the consumer won't accidentally ignore the case. (Deliberately, perhaps, but that's a people-problem rather than a technology one.)
Saying it's so doesn't make it true. You can declare that an error is simply a special value returned and expect people to check for that value all over the place, or you can declare that errors are (literally) "exceptions" and should therefore throw.
You can ignore error values returned just like you can ignore the numeric value returned from atoi. That's the point. They're just values. You are the programmer, it's your job to decide what to do with any particular value returned by a function.
It's also my job to decide what the square root of "fish" should be. What I do is use a declaration to mechanically verify that cannot ever happen. I never ever want to silently ignore an error, so why shouldn't I be able to declare that as well?
But exceptions are values too, i.e. you routinely have an object of type IOException. All of the patterns presented in the article can be done easily with exceptions.
I think maybe "errors are values" is a general principle and not meant to be specific to Go.
This article is bizarre. I've read it twice expecting an actual solution to null checking basically everywhere and it doesn't actually present one.
I'll stick with discriminated unions in F#, thanks. Errors are values, correct. But it seems hardly any languages actually bother to grok the concept. If your language doesn't provide type safety for ensuring that you capture all resulting conditions then it has failed its job.
Yes. Along with the vast majority of programmers being absolutely terrible at their job, to such an extent that they can't even choose an appropriate tool (language) to do their trade with.
Just look at how OpenSSL is written in C++. And look how that turned out. OpenSSL indeed. Wide-fucking-openSSL would be an even better name. Open to OSS contributions it is not; because the codebase is in such a terrible state. What is the point in OSS if nobody can contribute because it would take a year just to understand the ridiculousness of the codebase, right down to trying to understand why they chose such an inappropriate language for creating a supposedly verifiably secure software system.
142 comments
[ 2.8 ms ] story [ 213 ms ] threadEdit: I wrote an elaboration, https://news.ycombinator.com/item?id=8877732
So yeah, that's how he would know about them. They're extremely common in two areas of his interest (Go discussion/criticisms, language design).
A+
It's not pretty (and not in broad adoption), but neither is it impossible.
Also it doesn't allow for the use of already defined functions to do the transformations, (e.g. `maybe.flatMap(existingFn).flatMap(otherFn)` wont ever work, new closures will have to be defined that do casts before calling the existing functions).
So basically, its just-as-error-prone and just-as-tedious as checking error codes but in a different way. Both benefits of the pattern are gone. Given this, I don't think that the word "impossible" is a stretch at all (not having any of the benefits of the pattern = not implementing the pattern)
You could use Either, but it doesn't give you any guarantee that you'll get a string back, and the naming is lousy. (Left and Right? Really? I know there's a mnemomic, but we can do better than that.)
So, might as write your own ADT and name it right.
(Also, a real error-reporting mechanism should support cause chaining so you can see the dependencies; you can sort of do it with string concatenation but it gets awkward after a few levels.)
1. It's better not to use strings and instead use a user defined error type. 2. What do you mean "doesn't give you any guarantee you'll get a string back"?
> real error-reporting mechanism should support cause chaining so you can see the dependencies;
Can you elaborate on cause chaining, perhaps with an example?
(Go falls down on this too. Perhaps we don't need Java-style stack traces that go on for multiple screens, but we should have more than a string.)
http://doc.rust-lang.org/std/result/enum.Result.html
> (Also, a real error-reporting mechanism should support cause chaining so you can see the dependencies; you can sort of do it with string concatenation but it gets awkward after a few levels.)
http://doc.rust-lang.org/std/error/trait.Error.html
Either isn't custom-built for error reporting, so it makes sense that its naming only conventionally matches.
once per page? that's once per 30 or so lines. 2 or 3 percent of the code is this same repetitive structure? Also, only takes one time to forget this for everything to fall apart.
We've spent so much time trying to deal with uninit'd pointers and things like that on the language level, and here we have something that's much simpler, yet super repetitive and should be easy to catch on compile time.
Maybe I'm just a bad programmer, but I really prefer the Java "force you to catch exception" model much more to this.
>It's worth stressing that whatever the design, it's critical that the program check the errors however they are exposed.
If it's critical to check the errors, I think that it's worth writing language rules in a way to force people to write a "has error " code path (think pattern matching).
I get go is trying to be as small of a core as possible, but I think there's a lot of value in extra compiler checks (with no runtime cost!) to catch this sort of thing.
https://github.com/kisielk/errcheck/ will tell you if you silently discard the `err` value (though you can still explicitly assign it to _ to ignore). Seems worth using for a lot of projects.
It's nice because it's still easy to ignore the err in certain circumstances (e.g. the occasional `_ = f.Close()`), but there's always a visual marker to remind you.
And that 2%-3% doesn't include where errors should have been checked but were not. For instance run programs without stdout ("./program 1>&-") and most Go console programs will just happily write to nowhere whereas almost all Java ones error out immediately. Closing stdout doesn't often matter, but sometimes things like this are important.
I don't understand how in the 2010s it's okay to make a language with worse reliability. And worse "JavaDoc" equivalent. Go should be better than C and Java in every category and it's just not better in way too many.
We know how to handle errors better than this. Go's error handling is C style. We've since found exceptions, Maybes, and friends which are very powerful.
What you said implies that when Rust chooses to use Maybes they're somehow being bad at something. That's simply false; computer science as a field has advanced and one of these advancements (not even that recent of one) is improved understandings of how strong type systems can be used to make code, including errors, safer. Go completely ignores this of course and they're not doing it for a tradeoff that I can see. They gain nothing in terms of performance for not having this type (everything is already a reflect/Value anyways, so much for not having the baggage that you'd need for generics) and everything in terms of having a worse language.
Being good at something doesn't instantly mean you're bad at something else. In fact, I wouldn't even claim that's the general case.
If you disagree, you're welcome to point out the tradeoffs at work here. I think your comment is just a vaguery that is completely untrue in this instance.
One of Rust's goals was to remove the "feature" of null pointers. In its place rose the feature of tagged unions, to support the `Option` type. This is strictly trading one feature for another. Once this necessary task was completed, Rust was happily able to reap all the benefits of tagged unions elsewhere, such as with the `Result` type. It is not greater language complexity, it is merely the reuse of simple language features to empower developers to create more robust software.
Now, I am not among those saying that Go needs tagged unions, or any other feature for that matter (no, not even that one). I think Go is a good language, sometimes to the surprise of my peers. But what's disheartening to me is seeing a Go core developer imply that Go is literally the baseline for programming language simplicity, in effect papering over all the various tradeoffs that programming language design entails.
I think, and someone correct me if this is wrong, that implementing exceptions in Go is prohibited by go routines.
The stereotypical way to implement exceptions is using setjmp/longjmp which unwind the stack back to a known location. Since go routines are implemented as continuations, any particular executing go routine may or may not have it's stack in context, and I'm not sure what it means to unwind stacks across multiple continuations, ie is it even possible?
So that's one possible example.
No. Go already has all the runtime functionality necessary to support exceptions via panic/recover.
See for example the first answer to this SO question on error handling in Swift, which doesn't use generics at all: http://stackoverflow.com/questions/24010569/error-handling-i...
Why is not having stdout automatically an error condition? Lots of programs should work fine with no stdout. If a program requires stdout and doesn't fall when it's missing, that's a bug (and pretty easy to fix).
In UNIX the file handle an opened file gets is the next available, lowest number. Closing stdout means that the program opens a file and it gets file handle 1 instead of a higher number. Then it writes something to stdout, which is an alias for file handle 1, but instead it writes to the opened file.
I believe this caused some security bugs in passwd or maybe su where you could run it without stderr and "Password:" would be written to the passwd file instead of stderr. ...but these specifics are beside the point, which is that it is next to impossible to anticipate the problems an unhandled error can create.
Java's checked exceptions can be annoying when you have to add some extra "throws X" or handle every case, but my experience is that I never had the slightest doubt over how a method could fail or what needed to be done to handle errors. For unchecked exceptions maybe you have a point, although their failure mode for unhandled errors is still to abort the program rather than to continue on with undefined behavior as in Go.
It is easy to catch on compile time. https://github.com/kisielk/errcheck
Alas, the lack of those other features leads to articles like this one that offer unnatural ways of avoiding something that is pretty much idiomatic in the language.
If you log the error when checking if ew.err != nil, which might be many lines or even a different function, you'll have a hard time getting a meaningful context (which is hard to do in Go in the best case), like a stack.
And regexp uses panics to propagate parse errors up the stack internally, and that's even described in Effective Go: http://golang.org/doc/effective_go.html#recover
In both cases an error value is returned to the package user.
In general, I think you should feel freer do abnormal things that suit your situation within a package as opposed to across API boundaries. The standard ways of doing things should still be preferred when it's not too costly: the standards are what they are for a reason (even if the tradeoffs they represent are more appealing in some situations than others), it's easier to read code that's written in the way everyone's used to, and standards help keep you from spending time bikeshedding unimportant details. But the surface area of your package is the first thing users will have to learn, and what you have to keep in your head whenever maintaining code that uses your package--that's what it's most important to make clean and normal-looking and consistent.
I know the post talks about using an unusual error-handling pattern in an API, and you can do that sometimes, too. Just trying to make a separate point--when you're thinking about doing unusual stuff, keep in mind there's a big difference between an unusual implementation and an unusual API.
In general I'm a bit surprised about all the hate(I say hate because of how people react to it and react to suggestions for how to better work with it) the error system gets. I find that creative use of error/panic can usually scratch my "this is too much error handling code" itch when it flares up. As long as it's documented in the API I agree that unique public error handling strategies are not all bad.
Interestingly I write a lot of C#, which of course has try/catch/finally, and I find that if you are doing it properly you have many levels of handling and a strategy involved at the different levels for what should done when. I find that many people write TERRIBLE error handling. There may be try/catch sprinkled around but there will be no strategy to it with edge-cases and improper handling abound(masked exceptions, finaly's throwing new, unrelated exceptions, etc). I personally find the amount of effort required into developing and executing a robust error handling strategy to be about the same between C# and Go. That's not to say that I don't sometimes wish Go had C#'s error handling( ;) ), but I realize they are different beasts with different masters.
I realise the later was just laziness on my part, but the constant err checking really does force good habits on me, which of course yields higher quality code that's easier to reason about when something goes wrong.
That is why I like exceptions, so I don't have to explain possible errors like this if I don't have to, but can get a top level exception and see a stacktrace with it. Then choose to add aditional info.
I really like that pattern and use it quite a bit. For example, it is easy to turn code that connect into a piece of code that re-connects with a back-off without having to modify lower layers of the connect call itself (say because it is already in a library).
In a language with Sum types we'd represent this by saying that errorful computations adjoin an error type to the return type.
What's nice is that this automatically ensures the above invariant of err/ret is held: a value of type Either Error Int is either "Right i" for some valid Int i or "Left e" for some error value e. The first thing we check is the handedness of the result and thus know the result of the computation.Sum types like Either solve Boolean Blindness because we literally cannot get access to our return type Int unless it is valid: if we find that the result is "Left e" then all we have is an Error value.
(Ultimately what this boils down to is that product types like (ret, err) behave very differently from sum types and cannot replace them.)
If Go had generics we would also notice that the error-passing behavior of Either is naturally ignorant of the particular choice of types Error and ain't. We could manipulate it meaningfully (and indeed write the functionality used in this article) for any and all values e and r as the type Either e r. Then upon use Either e r would specialize to Either Error Int.
It's easy to argue that this is silly: we only need one error type and it is not difficult to replicate this code. I'll argue for now that only having one error type is at best confusing and at worst a lie. Different processes and functions have different exceptional states. Coercing all of this variation in error meaning into the same error type ensures that misunderstandings and accidental unifications will occur.
Honestly, I really love Go for making sure that errors were values: it is an incredible step forward for making exceptional cases more reasonable. Unfortunately, Go does not seem expressive enough to build the machinery which makes this style of error handling all of reasonable, non-repetitive, and non-confusing.
Atleast the last example with flush returning the error made sense, since that's an explicit "commit" operation that the user must call for correctness. However not every series of operations has a final commit operation, so this technique is unsafe in general.
(In fact even flush might be optional? I didn't check the docs.)
Edit: The docs for flush say nothing, but the docs for the Writer interface itself ( http://golang.org/pkg/bufio/#Writer ) do:
> After all data has been written, the client should call the Flush method to guarantee all data has been forwarded to the underlying io.Writer.
It is possible to make mistakes. Go isn't the kind of language that forces you to be safe and do the right thing all the time. Like all languages, it makes tradeoffs.
The argument seems to be "you should be checking for errors anyway", but I feel like there are basically two types of errors: the obvious kind that you're going to check for anyway (or get bug reports), like network disconnects and bad files, and the "how the hell did that even happen" exception. For the obvious exceptions it seems like a wash, other than the try catch block being a bit cleaner, but for the "hell the hell did that happen" error exceptions seem way more useful. I'd much rather my program explode immediately than risk someone letting the error passing silently because people didn't check err because like "that will never happen". I know, functions can call panic and all that, but it just seems clumsy for no net gain.
I do think there's one type of error checking that doesn't get enough love (I don't know if Go has this, but I'm guessing probably?). "assert" that gets removed in release builds. It's kind of silly when you're working alone, but in a team of programmers or for libraries it's awesome, because it's basically like runtime documentation that gets compiled out. So you can warn your fellow devs about things without it bloating your program. The newer C++ has a really great feature called "static_assert", which is basically a compile time assertion, which can be handy for things like checking in various places that maybe if the count in enum changed, that you accounted for it in another spot of the code, or that certain aspects of a new platform sit right, or things like that.
If you're programming in modern C++, then exceptions allow you cleanly (as in, clean code) leave your program in a very well defined state. You have to adhere to RAII, and there's a lot of C++ shops out there that don't allow RAII (and are wary of exceptions, because they ignore RAII). I've routinely used exceptions in C++ as a both efficient and clean way to propagate errors out to a stack frame that can handle them, fully confident that RAII will clean up as I go.
> and have some ugly performance implications
More than having an error object that the calling frame needs to check? If I understand Go (and, since it's mentioned here and I think is relevant, Rust's) setup, each stack frame between the frame that can handle the error and the one where the error occurred must check that an error did indeed occur, and if so return it. Rust makes this pretty through macros, and the article has some good thoughts on it for Go. Nonetheless, can the compiler compile that to better than an `if error { return error }` type check?
My understand of C++ exceptions is that this was basically how things _were_ done (but no longer). Previously, exceptions were managed by "sjlj" (short for setjmp, longjmp, the calls used to manage exceptions); any frame that _could_ have an exception would need to call setjmp on entry, and this was a bit hefty.
However, my understanding is that, at least for g++ on Linux, C++ has abandoned this in favor of a newer scheme called "dwarf2"[1]; this scheme is such that entering a "try" (or a stack frame where RAII would need to do actual cleanup) is free (rather, it is covered as part of the normal stack setup, really), leaving it gracefully is also free; only during actual stack unwinding do exceptions add code/performance penalties.
I think there is an unfortunate amount of information that C++ exceptions are inefficient left over from the sjlj days.
[1] http://www.x86-64.org/documentation/abi.pdf
I'm not against C++ exceptions, I just think there are legit reasons for shops to avoid it in that particular language . But I like exceptions in general.
Note that with java's checked exceptions, we get the exact opposite effect: lazy programmers catching every exception and either doing nothing at all with it or at best, printing the stacktrace and moving on. And it doesn't matter if you're not lazy yourself, the third-party code you reuse will do it.
Then there are exceptions that shouldn't even be checked, for example, you call UrlDecode and specify "UTF-8", gotta catch an exception in case "UTF-8" isn't a valid encoding name!
>try catch block being a bit cleaner
Except when you want to declare a variable and initiate it in the same line, but can't because then it would be scoped to the try block
Of the various approaches I've seen to cope with this problem, Rust's try! macro is the most elegant.
Given the practical minded nature of Go and the core team's willingness to add in special cases to the language where appropriate, I think a Go version of try!() would be welcome and fit with the ethos of Go. It solves the 80% case and it doesn't mean the creation of a type-safe macro system.
The thesis of the blog post is that errors are values and thus you can program around the repetitive handling of them. But the proposed solution of writing a one-off implementation of Maybe like `errWriter` isn't an answer because:
1) Most importantly, when you need to call 3 functions which return differently typed values and can fail, there's nothing in the language to help you. This is the common case. 2) The implementation is now less obvious. To understand it, I need to read the `errWriter` helper code. 3) The code space you'll save by writing a new Maybe type is negligible.
The rust Result idiom is easy to implement as well, and is used frequently in the database.sql package to handle nulls.
In this way, rust and Go are not that different. The biggest difference is the lack of a generic Result, which means you have a NullString struct with a Valid and String method.
Several such wrappers are present in the Go standard library, eg. template.Must:
http://golang.org/pkg/text/template/#Must
The thing is, without generics, you need to write a new wrapper for each type you're dealing. Add to that that this really isn't idiomatic Go style (code that panics all the time is frowned upon), and most people don't bother.
This is a pretty good illustration of both the point in the article and why some folks don't like Go. It's programming with error values; you can decide exactly what you want to do with the error, including panicking when you encounter one, and use the language's own abstraction mechanisms to write that code. OTOH, the reason why we have exceptions in most mainstream languages is because many, many C programmers wrote the same error-handling code over and over again, and eventually someone decided that it ought to be a language feature. The point of a programming language is to abstract out common patterns that you'd otherwise have to write repetitively.
That isn't what try! does in Rust. You are thinking of unwrap().
The equivalent of `x, err = expr; if err != nil { return err }` is very common in Rust; it would be written `x = match expr { Ok(x) => x, Err(e) => return Err(e) }`. It’s such a common pattern that a macro named try! was placed in the standard library for this incantation. Therefore you can write `x = try!(expr);` (or if you’re going to ignore the Ok variant, just `try!(expr)`).
Rust also warns you if you do nothing with a Result object; you can ignore the warning or suppress it, but it all makes it very much harder to ignore such error conditions. Go is one step up from C, where integral error-indicating return codes are the norm and are typically ignored, but I feel it’s still a fair way off the confidence that Rust gives me that I haven’t accidentally forgotten to deal with an error case. If you want people to get the message “Whatever you do, always check your errors!”, it helps if you design the language to make it hard for the user to forget.
(You people that know Rust, I’m ignoring the `FromError` part for simplicity. That’s only in libstd’s try!, anyway; libcore’s try! doesn’t have it.)
[1] http://golang.org/pkg/io/#Reader
[1] https://github.com/aturon/rfcs/blob/io-reader-writer/text/05...
The number of APIs making use of that pattern is very much infrequent, and doesn't justify it for the 99.99% of functions which don't.
> I think it is a very useful idiom particularly when dealing with things where failure is more normal
And you could have the exact same information by returning, say, `Either (Int, Error) Int`. Or even `Either (Int, Error) ()` since partial writes are apparently considered error cases.
Although I would really use a separate data type e.g.
and let the application decide whether a partial write is an error or normal behaviour.I can assure you that error codes in C are not typically ignored. What kind of sloppy C code are you writing?
If you have to call `write()` 10k times (say, in a loop), and get an error on the first write, instead of either unwinding (in C++) or returning (in Rust), that Go solution requires your program to check for nil 99,999 times.
Article posted 1 hour ago. Ctrl-F Generics = 3 matches
Macros like try! might take up less space, but is that really a bottleneck for your work?
I generally use Scala and just chain through Try[T] monads and address the sad path right at the bottom, right next to the culmination of the happy path. I always know where it is, I can't miss it, I'm not repeating code all over the place and running the risk of screwing up with part of it...to me it's just a smarter way to handle the failure case.
If you see only the success path as the "true code" then you are missing the whole raft of other conditions that can occur; its an idealised idea of what the code is.
This is why I like Go. Its the first language I've ever used that forces me to think pessimistically about my own code.
I am thinking about my error case. I am explicitly acknowledging that I am doing so; heck, scalac will fail to compile if you haven't folded across both the failure case and the success case. I'm just not writing the same (stupid, wordy, potentially failure-prone) boilerplate code every time I make a method call into a library.
These can be chained indefinitely. At the end you have a Try object of the generic type of the final mapping function, against which you can pattern match for Success or Failure. (Failing to compare against both Success and Failure is a compiler warning. There's a flag, IIRC, to make it an error, but since my IDE is already yelling at me that I screwed up I don't in practice have this problem.)
Additionally, at any point in the chain you can call `recover`, which is similar to `map` but for the exceptional case (Try[T] -> Try[T], with Success[T] passed through) if it's a recoverable error.
All error handling--as separate from error recovery, which is rarely in practice useful--is always, always, always at the very bottom of the function, right next to the happy path. It literally is impossible to miss, and does not require copy-pasting code after every library function that can fail.
There's no reason to check 'err' every time unless your language is just straight-up too dumb to do work for you. Your time is more valuable than the computer's in all but the most extreme of cases; you should act like it.
If this is the first language you've used where you give that sort of attention to errors, you must not have used and learned from the terrible mistakes of C, which constantly returns errors which are not required to be handled, just like go.
Heck, you must not have learned from bash where every second line is "if [[ $? != 0 ]]" or you just did set -e and fail hard.
For me, the problem with Go's errors isn't even the lines they take up, but the fact that they still don't accomplish anything more than C's errors.
Since C, we've learned that you can use exceptions to force errors to be handled, or Options + matching to ensure each case is handled or extremely explicitly ignored. We've learned that errors can have contexts like call stacks attached to them.
Go ignores all of that and goes back to the C way. It doesn't provide an easy way to chain errors or force them to be handled. It doesn't provide a good idiomatic way to differentiate between the majority of errors from the stdlib because it's discouraged to have multiple error struct types; rather you're encouraged to use errors.New and make an un-differentiatable error type with no good context. It's encouraged to return an err from deep in the stack without including any additional information.
In fact, Go's even inconsistent in how it uses errors in the stdlib. For example, if you look at io, you can see the following: http://golang.org/pkg/io/#pkg-variables
They define their variables as error types which are simply strings. That means you can compare with the constants (e.g. err == io.EOF), but if you add any extra info to that err, such as callstack, you can no longer compare with it. If you look at os, you see "os.IsNotExist(err)" which tells you if an err is of a given type. If you look at net ( http://golang.org/pkg/net/#AddrError ) you can see they implemented their own Error interface implementations which you check by doing `if addrErr, ok := err.(AddrError); ok { // handle error }`.. the fact that the Go authors included three different error patterns in the stdlib already shows that the entire error process in go is poorly thought out. In addition, none of the methods allow you significant flexibility in adding your own info to the error as you pass it up without losing information.
If you don't like try! in rust, you're free to write your errors out explicitly, but if try! fits then it's good to use it.
The majority of bugs in code come from people being unable to keep a sufficient amount of the program state in their head to track program flow accurately. If half of your program flow is errors, then no wonder. There's an old principal that each function body should fit, in full, on one screen because that ensures it can be read without scrolling and reasoned about fairly easily. Error handling wasting space also wastes thought and makes your code provably harder to reason about (if you don't think that's true, look at the incidence of 'mistake checking error return values in c' vs 'mistake checking error return in haskell' vs 'forgot to either catch or annotate throwable for exception in java'. Yeah).
So, you call them ignorant (incredibly ignorant, at that) because they're not using a language with exceptions or pattern matching, and they're wasting they're time on "C-style" error handling and... what? They're ignorant until they use something else?
You're also drawing a weak conclusion here: if you use Go idomatically, which requires more lines on your screen for error handling, and a majority of programming errors are a result of the programmer not being able to keep a sufficient amount of program state in their head because of code not fitting neatly on the screen, then using Go leads to more bugs.
Do you have anything that substantiates that?
Honestly, yes, though I would try to phrase it more constructively. If you've only seen C-style languages, it's easy to be ignorant of the world out there; it's easy to try two or three languages and think you know what's going on, when in fact the languages you know all come from a very similar lineage. Like learning English, German and Dutch, and then assuming you know the general principles of human grammar.
If you've tried a language pattern matching and higher kinded types, and found them to be not useful, then fair enough. But you owe it to yourself to try, and even if you end up not using the feature, understanding it will make you a better programmer.
> Do you have anything that substantiates that?
Over many years of research into what causes bugs, there's only one thing we've ever been able to correlate reliably with number of bugs: number of lines of code. Even across different languages, the correlation holds: http://programmers.stackexchange.com/questions/185660/is-the... . So the only evidence-based way to reduce software errors is to reduce lines of code - something you can do by switching to a more expressive language.
I also think you're correct about everything you said, especially about the inconsistent treatment of errors in Golang's stdlib, and the difficulty of differentiating between them. I've noticed the same and it has annoyed me. When writing my own application code I do maintain a consistent pattern for returning my own errors, but I have been frustrated when consuming the stdlib for this very reason.
Maybe as my code base grows I'll come to agree with you, but for now at least it seems to be at a nice middle ground where the error handling isn't overwhelming and it isn't swept under the rug. And to my knowledge, none of the bugs I've created when writing Go code have come from my understanding of the program flow being interrupted by error handling code. And I've probably avoided many bugs because of this pessimistic approach I take when coding, as jalfresi put it in his comment[1].
[1] https://news.ycombinator.com/item?id=8878852
Languages like Rust and Haskell do this right. Both make heavy use of optionals for failable functions, and Haskell's support for HKTs (and therefore Monads) allows awesome stuff like Maybe or Either chaining.
Monads allow you to write code that, syntactically, looks like you're not doing any error handling. In reality, the error handling semantics are baked in at the Monad level. This makes sense, because error handling is usually very repetitive (e.g. "Check if the function we just called failed. If it did, return early. Otherwise, keep going." ad infinitum). As a (pseudcoded) example:
If any of those steps fails, the entire computation will stop and the error message will be returned. This is roughly equivalent to checking for "nil" (in Go) after every function call, except you don't actually have to do it. This is defined in the code for the Either monad, which is at https://hackage.haskell.org/package/base-4.7.0.2/docs/src/Da... . As you can see, it's 4 lines of code.(Side note: this is pretty much how you write parsers in Haskell using the Parsec library)
Haskell doesn't do so enough, though (I don't know Rust and so won't comment), so that we still need manually to choose to use the safe library (http://hackage.haskell.org/package/safe).
Do you know anything about the motivation of those choices? Surely it won't have escaped anyone's notice, especially not the designers', that this is a wart on the language, and that there is an easy solution; so I can only imagine that the easy solution was rejected for its inelegance—which seems strange, given the devotion to theoretical purity of the rest of the language. (I contrast the situation here to that of the numeric tower, which is clearly flawed but for which, even now, the 'right' approach is not so clear.)
Lisp's continuations are also IMHO a great mechanism---you catch the error and then you can tell the function down the stack what it should do: you can abort, or you can try fixing the error and continuing, or you can try again for some number of times and then report a new error if that fails. It's more flexible than stuff I've heard of before.
http://www.lysator.liu.se/c/ten-commandments.html
Most of the problems with exceptions in C++ come from the C++ memory model - allocation and deallocation in exceptions are usually painful, and care must be taken to clean up and unlock things. Go doesn't have that problem - it's garbage collected.
Go does have "panic", which is Go's answer to "longjmp". People keep trying to use that as an exception mechanism, which is not a good thing.
Python seems to have the best track record with exceptions. Python's usual problem with exceptions comes from library functions which, under some rare circumstance, raise an unexpected exception and take down the whole program. This is mostly a legacy problem due to the poor original design of Python's exception hierarchy. You would like, for example, for everything that can possibly go wrong with an HTTP read or a network operation to be a subclass of EnvironmentError, as caused by an external event or data. That wasn't the case originally. There are still problems in Python 2.7 with getting a ValueError because some low-level data item was bad UTF8 or something like that.
The trouble with the "errors are values" concept is that error details tend to get lost as errors propagate upward. This came up yesterday on HN in connection with network errors for GoGo's airborne networking service. I once argued that D's error type should have a "why" pointer slot, so that, when you passed an error upward, you could link the lower-level error to the higher level error. The error message printer would then list out the errors, yielding something like
The problem of avoiding resource leaks is somewhat orthogonal to error handling, though. Do note that Python's "with" statement doesn't play well with "yield", which is an important gotcha. But in practice, it's quite convenient.
You've got to emit an error somehow and static checking ensures the consumer won't accidentally ignore the case. (Deliberately, perhaps, but that's a people-problem rather than a technology one.)
What I can find, is this: http://c2.com/cgi/wiki?AreDesignPatternsMissingLanguageFeatu...
(slide 4) "What Are Design Patterns: To avoid limitations of implementation language"
[0] http://norvig.com/design-patterns/
https://web.archive.org/web/20141224143931/http://norvig.com...
I think maybe "errors are values" is a general principle and not meant to be specific to Go.
I'll stick with discriminated unions in F#, thanks. Errors are values, correct. But it seems hardly any languages actually bother to grok the concept. If your language doesn't provide type safety for ensuring that you capture all resulting conditions then it has failed its job.
Just look at how OpenSSL is written in C++. And look how that turned out. OpenSSL indeed. Wide-fucking-openSSL would be an even better name. Open to OSS contributions it is not; because the codebase is in such a terrible state. What is the point in OSS if nobody can contribute because it would take a year just to understand the ridiculousness of the codebase, right down to trying to understand why they chose such an inappropriate language for creating a supposedly verifiably secure software system.