Errors are values
blog.golang.org
blog.golang.org
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.)
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.
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.[1] https://github.com/aturon/rfcs/blob/io-reader-writer/text/05...
I can assure you that error codes in C are not typically ignored. What kind of sloppy C code are you writing?
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.
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.
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.
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...
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.
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.
It is easy to catch on compile time. https://github.com/kisielk/errcheck
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.
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
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)
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.)
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().
Edit: I wrote an elaboration, https://news.ycombinator.com/item?id=8877732
A+
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).
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.)
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.
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?
- 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.)
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.
What I can find, is this: http://c2.com/cgi/wiki?AreDesignPatternsMissingLanguageFeatu...
(slide 4) "What Are Design Patterns: To avoid limitations of implementation language"
https://web.archive.org/web/20141224143931/http://norvig.com...
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'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.
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.
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".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.
Article posted 1 hour ago. Ctrl-F Generics = 3 matches
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).
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.
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).
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].
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.
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. 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.
A more fine-grained approach which is conveniently omitted from the article, BECAUSE IT'S SPAMMING `if err != nil` EVERY OTHER STATEMENT...
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.)
I think maybe "errors are values" is a general principle and not meant to be specific to Go.
Imagine if your Browser crashed every time it hit an error, like a network connection issue, writing to a file, etc...! That just wouldn't be tolerated by consumers of the software.