Banging errors in Go
flak.tedunangst.com
flak.tedunangst.com
Yes it means errors shows up everywhere in your code - the same types of errors are something all languages have to deal with, just accept our faulty reality for what it is and have the discipline to account for it.
https://go.dev/play/p/WbGeW8wSu0X
(Ignore the obvious SQL injection issues in that query, thx :)
It takes a lot to make me mad, but this inane behavior manages it. This behavior, singularly, makes it nearly impossible to achieve higher levels of ergonomic safety on the types of errors functions can return. You basically always have to just return the `error` type, and require callers to do runtime reflection.
And this can rear its ugly head all the time. You've got deep functions that are super specific and know exactly what kinds of errors they can return. You've got higher-layered functions that call lots of things that all return different error types, so those may just return `error`. Turns out; they all just have to return `error`, because if you make them more specific, at the freakin TYPE DEFINITION, a segment of the syntax that other sane languages would say is "compiled away" and "not relevant to runtime behavior", you lose any ability to guarantee nil comparisons will work the way higher-level callers expect.
https://go.dev/play/p/fb0e_4loDBf
Its a problem in any situation where a function broadens the type of a struct pointer its returning to an interface. Its just most commonly encountered with errors, because you can't just return `DatabaseError` there, because the zero-value is non-nil and everyone checks errors with `if err != nil`.
The internal Go style guide of a billion dollar tech company every single person reading this has heard of reads: Never return pointers to structs designed to be used as errors. Its a significant, real problem. Its antithetical to any reasonable understanding of how this code should work. Its antithetical to even unreasonable understandings. Blog posts which explain it start with "this makes sense when you understand how reflection works on interface-fulfilling pointer values" then launch into a twenty paragraph graduate thesis as if Go wasn't explicitly designed to help fresh-out-of-college engineers write productive and performant code for Google.
I like Go; but their recent statement that Go will never break backward compatibility genuinely scares me because I'm not sure they can fix this without breaking existing code; which means it may never get fixed. I'm doubtful the designers even consider it a bug. Just... ugh.
This is a common mistake when coming from exception-based-error-handling languages where exceptions are differentiated by type. In Go, if you want a more granular distinction between different kinds of errors, you don't use types, you use values: https://go.dev/play/p/ddzhAqRgK_1
If you insist on using the typesystem for granular error checking, then you can define an "Is(target error) bool" method on your custom type to differentiate in the same way: https://go.dev/play/p/gZmYgOq6wSo
Per https://github.com/golang/go/wiki/CodeReviewComments#interfa...:
Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types
There clearly is this one exception ONLY for the `error` interface. This advice plus the weird runtime check conventions like `Is` / `As` are nowhere to be seen for other interfaces in the ecosystem.Go makes a lot of effort so that errors are special. There is an exceptional tuple-type that exists solely for returning errors, and the error-return-interface convention breaks interface-return conventions. If they implemented sum-types, then we would have the typical `Result<T, Error>`, and then go programs would have much more invariants and be much more composable. But presumably compilation- and/or run-time would be worse.
Another alternative would have been adding syntax sugar like rust's `?` for `if err != nil { return ..., err }`, and `match err { case CustomError: ... case error: ... case nil ... }`. And some helpers for composing/piping functions.
- There are genuine use cases where returning both a value and an error makes complete sense. My favourite example is doing I/O: some bytes were copied, but there was a problem with the rest.
- It has significant overlap in scope & functionality with interface types. You could have "type Color enum { Named(string); RGB(int, int, int) }", or you could have "type Color interface { RGB() (int, int, int) }; type NamedColor string;" etc and it's not very clear which style you'd be supposed to use, and in what case.
The first problem could be somewhat alleviated by having very special result types that can have both a return value and an error, but these would have to be different from regular "variant" results, so you'd end up either with two ways to do a very similar thing, or yet another layer of abstraction.
The second problem is fundamental to the design of the language, and I would absolutely *hate* to see Go go through the same unholy mess as Python: "old-style" vs "new-style" classes, dataclasses (with third-party "attrs" as a stepping stone), protocols/generic containers, "match"/destructuring, all of these were clumsily grafted on and code that mixes all of these styles can be found all over the place. You do need to make ADTs and pattern matching a first-class citizen in 1.0, otherwise the place you end up in is not pretty at all.
Go? Yeah, we have a lot of SortIntegers([]int) and SortStrings([]string) and SortByName([]Person) in older code, but that's about its biggest sin. As of 1.21, slices.Sort & slices.SortFunc are in std and fixing this older code is pretty much a mechanical task. It's not perfect, but at least it's not worse by trying too hard to be perfect.
I don't feel anyone is saying that the addition of sum types would have to replace the existing (Result, error) pattern. Its just a tool in the toolbox. In fact, I think having both is really interesting from a function signature communication perspective; if a function returns (Result, error) in a post-sum-type world, that hints to me that the Result might still be useful even if an error is returned.
> I would absolutely hate to see Go go through the same unholy mess as Python: "old-style" vs "new-style" classes,
Anytime someone brings up how horrible the Py2 to Py3 transition was, I will remind them that Python is the most popular programming language in the world. Clearly; the transition wasn't actually bad enough to negatively impact its popularity; yet its the token example of "don't break existing programs, you don't want to be like Python".
That take is just counter-intuitively wrong. Its like the SDLC paradigm of "releasing often reduces bugs"; it feels wrong, but its actually right. I'm sure saying "we'll never break existing programs" imparts a nice warm feeling in your heart, "we're mature, unlike those other dumb languages". But there is extremely little evidence that backward-incompatible language changes hurt the adoption of programming languages. Actually; there's substantially more evidence that languages which can't adapt and evolve to change will eventually die.
I'm not asserting that will happen to Go; I think they're good about bringing forward enhancements to the language in ways that don't break existing programs (like generics). But I also strongly believe they need to rethink their "we won't break existing programs" rule. I am begging the Go team to break my programs. Its not a big deal. I just can't imagine still writing Go programs in 2050 like we are today; it leaves so much value on the table, and we can do better.
How do you reconcile that with the general rule or convention that `if err != nil` the value is unsafe to use?
Apart from this, I don't think sum-types should be added this late either. I should have written "if they had implemented". "old vs new style" does more damage than good. Today, it is what it is.
What I do think is feasible is adding some more syntax sugar around errors.
It's a contrived problem, at least. In reality, your code is going to look more like:
func GetUserByID(id string) (string, error) {
s, err := QueryDatabase(fmt.Sprintf("select * from users where id = %v", id))
if err != nil {
// Do something with the error, returning a new error if necessary.
return "", someNewErr
}
return s, nil
}
The callers of GetUserByID have absolutely no concern for the implementation details of QueryDatabase. When requirements change and you replace QueryDatabase with QueryWebService, callers expect to still get the same errors back, not HTTP errors all of a sudden when the code was previously getting MySQL errors. That would be plain horrible API design. So, you wouldn't actually ever encounter this particular issue in practice (assuming nobody hates you).But the issue is that the source of the problem is in lower-level code which made the theoretically correct decision to be specific what you return and generic in what you accept. That lower-level code cannot guarantee that its higher-order callers will wrap the errors it returns. So; the lower-level code has to remain generic.
Obviously my example is contrived; but the problem is not. It is allowed, but essentially never safe to communicate to the compiler the error types your function returns. Its ok to use custom error types; you just can't tell the compiler about them. This is a problem that is so deep in the language it has influenced how error types and functions are designed in the standard library. I'm aware of one production outage related to this problem (yeah yeah, blame bad reviews and bad testing, I get it, it still happened). I've caught it a dozen times in code reviews (especially after that happened).
Its a real problem. People don't write perfect code.
Yet another case of that old pitfall. Untyped nil uses the same keyword as typed nil, when they are not equal. It's an unfortunate contradiction within the language.
And it means that the entrenched practice of
err != /* untyped */ nil
is an overly specific check (nil error is harmless whether it contains the type information or not).Someone, somewhere has thrown at us a principle ("the theoretically correct decision") and someone else has thrown at us a conflicting principle ("err != nil is a universal idiom").
The problem: these are not principles! Both of these are decisions with tradeoffs. Who is making these decisions? Some blog authors or conference speakers? No: the team that owns the code. So, either:
- have a linter that catches *ErrX return declarations automatically,
- or, have a linter that prevents err != nil (generally: interface != nil)
Since the latter is impractical today, one needs to decide the former. Solved, isn't it?
It is safe as long as you don't write too much code for no reason. The problem in the example is that DatabaseError needlessly defined an Error method. It serves no purpose other than to allow the issue to arise. Remove said method, which has no reason to exist, and the program will fail to compile.
> Its a real problem. People don't write perfect code.
Okay, sure. It is possible that a programmer may, for whatever reason, leave out entire blocks of logic that the program needs. Imagine not adding billing logic to your storefront software – I'm sure it has happened to someone before! Making interfaces more intuitive does not solve that problem, though.
var errConcrete *strconv.NumError = nil
var err error = errConcrete
err != nil // true
Please fix this one.The problem was that I had a variable of an interface type and in the code was producing different objects implementing that interface. Now, a nil value was also allowed if none of the cases matched and then the variable was stored in a sync.Map as a value.
The problem was that when retriving the value the nil check never matched. Why? Well, it's type is defined as the interface even if it doesn't point to any implementation.
I get that the designers might not have considered a different option: it is allowed to invoke a method on a nil object, making what some languages would call static methods. Still, I agree with the grandparent: I think this is Go's biggest flaw, simply because it is so surprising.
func QueryDatabase(sql string) (string, *DatabaseError) {
Returning *DatabaseError instead of error is a pretty clear programmer mistake, which should be caught and fixed by any reasonable kind of code review.---
Long explanation:
In Go, "error" is an interface - i.e. a type that has an "Error() string" method defined on it.
The custom defined struct, "DatabaseError" is not an interface - it's a struct, and it has an "Error() string" defined on it. Therefore, any value of "DatabaseError" (or "*DatabaseError") type fulfills the "error" interface, and can be cast to a non-nil "error". Even the nil pointer to "DatabaseError" - you can call methods on a nil pointer, therefore it's a valid non-nil interface.
The problem in the code is the implicit cast of "*DatabaseError" struct into the "error" interface in line 21., which assumes that nil pointer is the same as nil interface. It isn't. The solution is either to 1) return "error" instead of "*DatabaseError" in "QueryDatabase" (https://go.dev/play/p/fb0e_4loDBf), or 2) to explicitly check the return value of "QueryDatabase" for nil pointer before cast (https://go.dev/play/p/TgikAk1mSn0).
I prefer the former approach, because even if you're free to write your own "error" interface implementation, you're still supposed to use it through the "error" interface, not the struct pointer directly. Interface is more than just a struct pointer.
P.S.
As I explained in the other comment, the intention of the original comment is to differentiate between kinds of errors through the type system. That's not the Go way - Go avoids type hierarchies as much as possible. The proper way to differentiate errors in Go is through values: https://go.dev/play/p/ddzhAqRgK_1
Or, if the user insists on having a error type hierarchy, define a custom "Is(target error) bool" method on each custom error type: https://go.dev/play/p/gZmYgOq6wSo
- a typed nil: "I know the type, but there's no value"
- an untyped nil: "I don't know the type and there's no value"
But when you compare these two, they are not equal. Sigh.
(Correcting my wording: "untyped" is not precise enough, because it's "I don't know the type exactly, but I know that it's one of the types that fit X interface". So maybe more like "nonconcrete nil".)
package main
func main() {
foo := nil
_ = foo
}
Raises a compilation error: ./main.go:4:9: use of untyped nil in assignment
It's just that zero value (nil) of interface is not the same as a zero value (nil) of a pointer.Casting a nil pointer to an interface does not create a nil interface, in the same way that casting a zero integer or an empty string to an interface{} (any) doesn't create a nil interface{} (any).
I code solo, so I'm not sure how others do it.
The alternative is to have twenty methods?
I build a data structure, modify its state in a dozen ways and then return it to the caller.
Reasons:
- It's simple and easy to understand. There's no hidden complexity, no magic, no gotchas. Nothing is going to surprise me about it.
- It's a very readable cadence. Do the thing, check the error, do the thing, check the error, do the thing, check the error. Once you get used to the error checks it's extremely readable.
- Despite all the extra characters the actual mental load of the error checks is tiny. Yes it's more typing, but the hard bit about code is thinking not typing, and it doesn't make more thinking.
- If I have to do something special with the error, it's easy. There's no incentive to handle this error just like the rest, or ignore it and let the exception handler catch it. If this error needs (for example) extra logging then I can just put it in there for this error check, no hassle.
- It's pretty much standard across all Go code. If I have to deal with someone else's code, I can expect the same cadence, the same simplicity, the same readable pattern of error checks. One of the great things about Go is that it is opinionated about stuff like this.
Many effectful actions e.g. reading from a file system can have a range of different errors each of which you want to handle differently e.g. out of disk space versus lack of permissions.
It's great that you're treating errors as values. But you need pattern matching and other techniques as they make your code more: (a) readable, (b) safer, (c) simpler and (d) less verbose.
The hilarious thing is that eventually Go is going to get these because there is nothing but upside. And then at that point you're going to wonder how you ever survived without it.
The go way to do this would be:
switch {
case errors.Is(err, outOfDiskSpaceError):
// handle out of disk space error
case errors.Is(err, lackOfPermissionsError):
// handle lack of permissions
...
default:
// do the equivalent of the `_` case in a scala match statement or `t` in a lisp cond
}
Which is roughly as readable as scala/rust's match statements. Moreso if someone tried to get cute in scala and bind both the object as a whole and parts of it at the same time ( something like `case x @ Type(_, _, y, _)` ) or when people get real cute with the unapply method.I mean, I like scala. It's fun. But I would never say it's more readable than go. I've been left to support things that happen when a team of average intelligence devs get a hold of it.
You're also really comparing an MIT and New Jersey style solution here, and judging them both on MIT merits. And I don't think that's exactly a fair argument.
PHP tried to correct the mistake of not giving some more thought to the switch statement at the beginning by including a new "match" expression in PHP 8 - fun times for everyone who used classes called "Match"...
I use a language that has both pattern matching and case constructs. I use both.
If you're doing complex pattern matching all over the place (especially matching the same sets of cases repeatedly in multiple locations in the code), maybe your design sucks. It's not making effective use of OOP or some other applicable organizational principle.
Yeah, but when you need to bash someone over the head, the rocket suddenly isn't any use anymore.
IOW, sometimes the simpler thing is better.
In fact, in practice, the simpler thing is usually better... Like, rocks are usually more useful to the average person than rockets are.
struct BinaryTree {
leaf_value: int,
left_child: BinaryTree,
right_child: BinaryTree
}
function sum_leaves(tree: BinaryTree) -> int {
if tree.left_child != nil {
return sum_leaves(tree.left_child) + sum_leaves(tree.right_child);
} else {
return tree.leaf_value;
}
}
vs: enum BinaryTree {
Leaf(int),
Branch(BinaryTree, BinaryTree)
}
function sum_leaves(tree: BinaryTree) -> int {
match tree {
Leaf(leaf) => leaf,
Branch(left, right) => sum_leaves(left) + sum_leaves(right)
}
}
(If you're thinking "the first example should be using inheritance + polymorphism", imagine that "BinaryTree" is in a different library than "sum_leaves". If you're now thinking "visitor pattern", sure, go write your hundreds of lines of boilerplate code if you like.)The first example is less safe because it's filled with invariants: left_child is nil iff right_child is nil, and leaf_value should only be accessed when they're nil. The second example has zero invariants. (You might think there would be an invariant that the children aren't nil, but languages with pattern matching tend to use Optional instead of nil, so that invariant isn't necessary.) If you make mistakes about when you access various fields in the first example, you'll be accessing leaf_value when it's uninitialized, or get a null dereference from one of the pointers.
As for readability, that's in the eye of the beholder, but I find the second example a lot more readable for the same reason: it's clear in both the data definition and the use site which fields exist.
All sorts of details vary across languages, even with a small example like this, but that's the basic differences.
In terms of errors, though, it's generally "the result is either a value and no error, or no value and one of these errors". I get how sum types would help with this, and I'm not arguing against that; they would be useful. But the pattern matching basically still has to deal with that outcome, and have a pattern for each error type. It doesn't strike me as being inherently safer, more readable, etc.
I agree that sum types are lovely and that pattern matching makes them nice to work with but I don’t think you really make the case well here that it’d be superior rather than just personal preference.
Much like other FP features, shoehorning pattern matching into a language doesn't give you nearly the same advantages as building a language around it, so I don't know that it would make Go significantly better.
The hard part is understanding the existing code. The more cluttered and verbose the code is, the harder that is. Go's boilerplate if err != nil return err, nil becomes something that your eyes just skim over - which is fine right up until you have some code that's doing something similar but not the same, and don't even notice.
I do have to spend a second reading the action if it's not just `return result, fmt.Errorf("failed to do the thing: %w", err)` but that's good, I think.
And all of this is way easier than trying to trace up through the stack to the nearest exception handler and work out what it will do with the error
edit: also, verbosity doesn't make code harder to understand, imho. If anything the other way around. Packing 5 statements into a single line is massively harder to read than separating those same 5 statements into 20 lines with error handlers.
You still have to do that part though? Like, this function returns err, so the caller returns err, so the caller of that returns err, ... - you've still got to walk up the stack to the point where the error is actually dealt with.
> edit: also, verbosity doesn't make code harder to understand, imho. If anything the other way around. Packing 5 statements into a single line is massively harder to read than separating those same 5 statements into 20 lines with error handlers.
Very much not my experience. There's a huge understandability hit when a function doesn't fit on a single screen and you have to scroll, so vertical space is really precious.
This is why we wrap errors. The error message gives a pretty good indication of what the stack was doing when it went wrong.
I had a junior dev work with me on some JS. I was writing it in functional style because it made sense at the time. He was really struggling, so I refactored it to old-school imperative and he understood it and was able to work with it. It might have been an issue with the way he was taught JS, but I think it's more that tightly-packed concise code is actually harder to parse. Not least because you have to understand the whole thing to work out wtf it's doing. Whereas with one-statement-per-line you can scan down to the lines you're interested in and focus on those.
OTOH, if you make sure you do something like
if err != nil {
return fmt.Errorf("what I was doing when the error happened: %v", err)
}
then you get very precise targeted errors appearing in logs that are much easier to track down and fix than either just returning the error or the usual generic catch block found in other languages.IME properly handled Go errors make for much more maintainable code.
IMO the real value in proper error handling (ie what makes fixing errors easy) is that context, not the precise line # of the error which, though useful, is supplementary data. Rust errors with anyhow::Contexts are far more workable than those without.
Edit: I actually wrote a simple Result handling package for Go that includes error context and stacktraces: https://github.com/kitd/chock
Golang has alot of good things about it. This is not one of them and is a wart on the language that is tolerated because the genesis of the language is to be an entirely inverted approach to verbosity than Java. Its not something to be praised.
It's easy to look at a 50-line function with one statement every 5 lines and find the bit I'm interested in, because it's easier to screen out the bits I'm not interested in. Rather than unentangling a 5-line function that has 10 statements in it, because I have to work out what all of it does in order to understand it and I can't focus in on the bit I'm interested in.
There absolutely are gotchas, exactly because they are not sum types. There are functions where both “slots” are used as return types, when an error occurred.
> It's a very readable cadence. Do the thing, check the error, do the thing
Arguably, you can’t reasonably handle most errors in-place, you just don’t have enough context for that. Also, you want to make your business logic right — all those verbose, often incorrect/naive error handles will just make it harder to read your own logic. Also, very easy to accidentally swallow an error - exceptions/sum types are much better in this regard, you can’t not care about them.
One thing you can do is go to the Go slack space at https://invite.slack.golangbridge.org/ and ask for a review in the #reviews channel; 20 error checks is a lot.
You've just described Scala which inspired many of Rust's features.
Swift I suppose although it doesn't make much sense outside of the Apple ecosystem.
Sometimes you want to return a partial result along with an error. Go's idiom of returning multiple values, with the final value being an error, allows this situation to be easily supported.
Rust is actually largely error-unaware. It does have some syntactic sugar (mostly `?`, and even then that’s not restricted to errors), but for the most part Result just an enum with a `must_use` annotation, everything flows down from that.
There _is_ some futzing that sometimes have to happen due to various operations' Result types using incompatible Error types. That's just a thing that has to be dealt with.
Being able to define your own app-specific Error type which everything gets converted to is incredibly useful and powerful. Especially for web apps where you can return the correct response codes depending on the error.
It's also often taken care of by a sort thiserror macro invocation per "inner" type. There are obviously more complex error setups, but this covers the vast majority IME
I get why this is, but it does make me miss the Python Exception model of "there's ~15 base exception types. One of them is probably good enough for you". One could point out that the arguments are usually "just" strings there too, but at least there's some conventions.
I understand Rust's philosophy, I just find it annoying.
For bubbling up errors you have the ? operator, then on results you have map, map_err, and, ok_and...
What i don't like is where approximately 3/4 of the significant lines of code are endless repetitions of:
if err { return nil, err }
Rust has ?, haskell has do notation, scala has for. All of them have higher order functions for operating on the result. But go is not only more verbose, it is more error prone, since it is easy to forget to check an error, or use the other returned value before checking the error.
3/4? How are you managing to have so many cases of blindly passing an error up the stack without introducing problematic coupling?
I would suggest that if you are able to realistically do this more than a couple of times total in an application, you have introduced way too much pointless indirection and should take a closer look at your overall design. Something is amiss.
And that goes for any language – not something exclusive to Go. Blindly propagating an error up the stack using exception handlers, for example, is prone to the same problematic coupling and indicates the same design problems if seen in more than rare situations.
* Handler maps the wire format message to an internal entity. Return a bad-request type error if it fails.
* Handler calls controller. Perhaps some specific error conditions get dedicated status codes, the rest get 5XX.
* Controller calls gateway. Except in rare cases where the external call is optional, you probably just bubble this to the handler.
* Gateway maps internal entity to wire protocol format request. Sometimes this transformation can have errors; bubble these up.
* Gateway calls wire protocol client. Wire protocol client may have built-in retries, or gateway may have application-level retries. In any case, if retries are exhausted, bubble this up to the controller.
* Gateway maps wire protocol response to internal entity. Depending on the schema, this can often fail to be well-formed, even if the request is "successful." These errors also need reported to the controller.
* If everything is successful up to this point, controller calls repository. Bubble errors to handler.
* Repository maps internal entity to storage model. Sometimes this transformation is also fallible.
* Repository calls storage client. These errors might be retryable but after retries, need bubbling up.
* Finally, storage client returns successful value to repository returns to controller returns to handler returns to wire protocol server.
This is just a hello-world level microservice. We have thousands of them, with easily a dozen endpoints each and probably 5+ interactions per endpoint on average. And oh yeah, every single one of these error return sites needs a unit test case.
I don't think you should blindly pass up all errors, but IME propagating upwards is usually the right thing to do in your middle layers.
If I am using a library, I pretty much expect each and every one of its API to return a Result<T> or Option<T>. Those that don't either: do something funny to hide the bad states or simply crash and die which means I have to do some double checks on my end.
As a sysadmin, I would love for the code to log/print the "#€%"#€%#€ filename when open() or read() fails, and not just bubble up some generic "something went wrong, fix something" and have me dive into strace/truss/ktrace just to know that /home/foo/badperms.txt could not be opened.
For some reason, all these wrapper libraries and frameworks and stuff are super good at hiding things for which we used to get decent errors, like "could not open tcp port 443" or "file: ./badperms.txt open() failed" but as the layers stacked on top of eachother more and more, the code calling "set-up-totaly-secure-sending-of-file-to-remote-http-endpoint-and-renew-LE-cert-if-needed()" has so many moving parts that the program can only say "worked perfectly" or "dang, noone in the world knows what went wrong, try again tomorrow, worked on my laptop once before deploy".
So while it is not "fun" to handle all these particular errors one by one, when we stop fussing about details, we make someones life miserable as the filesystem goes full/quota, or when networks/firewalls hinder traffic if we can't even tell the user which of those two occured because it would be "tedious" to pay attention to so much detail when all I wanted was my program to be short and sweet.
There are rare circumstances where it is the right thing, but if you are seeing more than one or two instances in a substantial codebase, something isn't right. If it is a common occurrence, and you are not purposefully trying to demonstrate your hatred of future developers, you've no doubt introduced way too much unnecessary complexity – which too is going to make life miserable for future developers.
Frankly, the entire Go community is no doubt keen to hear it. The 'try' proposal fell apart because nobody could figure out a good solution to that problem at the time, and could not find justification for a whole new feature for rare occasions. If `if err != nil { return nil, err }` were to actually become tenable in most cases then said proposal could be revived based on your information. It was otherwise well received.
return nil, err
I'm fine with error values or Pythonesque exceptions, just not both combined.So much of my code is typically:
result, err := someFunc()
if err != nil {
return nil, err
}
Most error conditions are basically unrecoverable anyway. Errors : Fine.
Panics : Weird because golang already had errors.
Segfaults: Gah! (though this is a cgo issue apparently)
See
https://news.ycombinator.com/item?id=37908655
and
https://rachelbythebay.com/w/2023/10/16/env/It is true that panic allows any value to propagate, so technically you can use it to carry errors, along with anything else you can imagine (names, email addresses, audio, whatever).
But the intent is for it to be used for communicating exceptions. You will notice panic's behaviour mirrors exception handling systems found in some other popular languages. But errors, along with names and email addresses for that matter, are decidedly not exceptional.
You have the option to explicitly handle every error, not handle errors for certain methods, or bubble up errors to a single error handler or any combination thereof.
I was trying to remember who it was, but one author I thought had recommended subclassing every exception as a Runtime exception for this reason.
NullPointerException don't really make sense as checked because almost every method will have some exposure to null values. But the idea is that encapsulating methods can check for those and translate them into other exception types or just let it be handled by a global exception handler.
Java is not the best for error handling by any stretch. But it's easily better than Go.
Nah. If you ever worked in a big enough company you would have:
try & catch Throwable at the main entry.
Go forces to think about every error.
C# doesn't even have checked errors, after using Java for 10 years - checked exceptions are dumb.
Go doesn't have a stack trace in the error and thats dumb.
Unless you just always return it up the chain without thinking about it like the bulk of the code I've encountered. No more thoughtful than "catch and reraise" or "just throw."
And like in Java, an NPE ain't getting caught by the error return of a function in Go either.
With checked exceptions you need to either handle them directly or yes you can wrap them in a RuntimeException and catch it in main.
I've done a decade of old-school JVM development at enterprise companies. Never seen any codebase where no exceptions are locally handed.
The more that this has become my reality, the more I care about actually producing worthwhile errors. I'm not at all bothered that 70% of my code is error case management with some liberal sprinklings of context into those values.
At this point, I see straight through them in my code, to the point where I'm actually going to be confronted with an error, bug or emergent system case and I'm going to be exceptionally pleased that I put so much effort into actually managing these errors correctly and not just punting them up through a common and often cryptic common error handler case with most of the useful context about the detailed error environment now missing.
Error checks become a syntactic formalism that are easy and quick to type, easy and quick for the eye to scan, but have just enough presence to make sure you think through your error situation whenever and wherever you need to.
Go gives me a confidence in my code's error handling that is harder to get in other languages I've used, where the error situation is typically muddier.
Nobody would be making you use the operator all the time. In the rare cases when you need to do that, just don't use it.
Instead of
value, err := foo();
if err != nil {
// handle err
};
you get value := foo() catch err {
// handle err
} if value, err := foo(); err {
// do something with err.
} else {
// do something with value.
}If you end up with complexity at this point then it is time to refactor into several functions so the 'happy path' follows the standard idiom.
Consider it a Go code smell.
Right, and that's why it's bad.
Actually, maybe the IDE could (optionally) display it this way, so it’s not even a rewrite. The downside would be possible confusion when using another tool (like doing a diff).
It’s still not as good as having Either/Try monads available to avoid the mess of “if err != nil” madness but having gone back to Java lately and the issues we have with exceptions there, having errors as values instead of a magical control flow feature makes the code easier to understand.
Now that I can make sure I include slack traces on them so it’s easy to find where they originated I have the best of exceptions in place as well.
The C++ type intended to be similar to Rust's Result is std::expected
My rule for errors is to add only information that the caller isn't aware of. So don't do:
func GetFooByID(ctx context.Context, id int) (Foo, error) {
row, err := LookupRow(ctx, "foo", id)
if err != nil {
return nil, errors.Wrapf(err, "GetFooByID id=%v", id)
}
return row, nil
}
The caller already knows the id it's looking up, and that the name of the function is GetFooByID. It might include those in its wrapping of that error, but GetFooByID shouldn't.Meanwhile, this is good:
func UpgradeFoos(ctx context.Context, tx *Tx) error {
foos, err := GetAll(ctx, tx, "foos")
if err != nil {
return errors.Wrap(err, "GetAll")
}
for _, foo := range foos {
if err := UpgradeFoo(ctx, tx, foo.ID); err != nil {
return errors.Wrapf(err, "UpgradeFoo(%v)", foo.ID)
}
}
return nil
}
Now your logs look something like "server failed to startup error=apply migration: upgrade foos: UpgradeFoo(42): i/o timeout" instead of "server failed to startup error=i/o timeout". This saves you from "hmm, maybe it's too slow to get all the foos in a batch like that, we should change that". But nope, it's actually UpgradeFoo(42) that's broken.Wrapping applied well is my favorite feature of Go. Most people don't do it. The standard library doesn't follow my rules. But if you do it this way, every error you see is so easy to debug and resolve. Less downtime, more reliability.
(As an aside, that loop where you accumulate errors can easily accumulate into a multi-error as of recent versions of Go. I always prefer to try everything possible and return all the errors, then you can fix multiple problems with your input on one go. It's also good for cases where you are doing a main operation and an ancillary operation, like closing something. People often ignore errors on "Close" and "Sync", but by joining those into a multierror, then you no longer have questions like "there were no errors, but this file isn't on disk". https://pkg.go.dev/go.uber.org/multierr#hdr-Deferred_Functio... is a really nice approach for the common case of "defer fh.Close()". I have an `errors.Close` wrapper I use: https://github.com/pachyderm/pachyderm/blob/master/src/inter.... Can't live without it!)
We have no standards for this at our place and I think this might be a nice starting point to introduce something.
foo().unwrap_or_else(|err| {
// handle err
});
or foo() catch |err| {
// handle err
}I don't like the first first one. It's too verbose, and it looks like it's trying to introduce a shorthand for lambdas.
if err := foo(); err != nil {
// handle error
}
However, this only works in that particular case, if foo() also returns a value you can no longer use this because the value (and err) are scoped to the if block.this is the sort of silliness that is common in the Go world. as noted elsewhere, errors are values in lots of languages - Rust, C, Scala, Haskell, etc etc, but Go explicitly has no way to handle them nicely, no specific syntax and no fancy type system stuff like sum types.
it is my very strong belief that this will eventually be fixed in Go and when it does, almost all the people currently saying "I like errors being values [and it's fine that Go makes it very annoying]" will quickly prefer having some actual language help for these values.
var innerErr = errors.New("inner error")
func innerFunc() error {
return innerErr
}
func outerFunc() error {
err := innerFunc()
return fmt.Errorf("outer err: %w", err)
}
func main() {
err := outerFunc()
fmt.Println(errors.Is(err, innerErr)) // true
}
I don't think the Go community disagrees that it would be nice to make error handling a bit easier, because that was the top issue raised in the recent developer survey. The problem is that every proposal made so far either did not actually make error handling that much better, by looking at the feedback they received.Here's the same function in Rust:
fn decomp(filename: &Path) -> Result<Vec<u8>, io::Error> {
let fd = File::open(filename)?; // File is automatically closed by its destructor.
let zd = GzDecoder::new(fd); // flate2::read::GzDecoder::new does not return an error.
let mut data = Vec::new(); // Rust makes the caller allocate the buffer for reads.
zd.read_to_end(&mut data)?;
Ok(data)
}
I think this is great. From a reader's perspective, it can dramatically improve readability in a lot of functions. From a writer's perspective, these kind of solutions make it easier to compose expressions without interleaving if-statements after every other line.Yes, sometimes you want to add context to your errors instead of using this syntax. But you can always use verbose syntax when it's needed, and terse syntax when it's not.
I don't think this is true in this context. The buffer initially will have a capacity of 0, and will grow to fit the available data, so that as read_to_end is inserting data, the buffer will be resized until all the data fits.
However, if we had preallocated the buffer, or were reusing an existing buffer, then the buffer would only be grown if the data being read was too large for the buffer. In addition, there are other functions that can will never resize the buffer, and read only until the buffer is filled.
Perhaps a better way of phrasing this is that Rust lets the caller control where the data will be written to.
For the sake of a terse inline comment, it might be better to just s/allocate/create/.
read_to_string(path).context(ConfigFileSnafu { path })?;
[SNAFU]: https://docs.rs/snafu/latest/snafu/ use thiserror::Error;
#[derive(Error, Debug)]
enum Error {
#[error("Could not open given decomp file: {0}")]
FileOpen(#[from] std:io::Error),
#[error("Compressed read error: {0}")]
CompressedRead(#[from] gz::Error)
}
fn decomp(filename: &Path) -> Result<Vec<u8>, Error> {
let fd = File::open(filename)?; // File is automatically closed by its destructor.
let zd = GzDecoder::new(fd); // flate2::read::GzDecoder::new does not return an error.
let mut data = Vec::new(); // Rust makes the caller allocate the buffer for reads.
zd.read_to_end(&mut data)?;
Ok(data)
}1. How does it know how to create your Error enum? I guess it's from the #[from]? 2. What happens if your method tries to return something that's not an io::Error or a gz::Error? I guess the compiler catches that? 3. How would you handle doing this for multiple methods in the same file? Would you rename your enum to DecompError or something to avoid conflicts?
#[from] is just a convenience library feature, in reality it’s because of the From conversion trait which ? invokes on the way out. Essentially it calls ReturnType::from(ValueType) to bridge the two.
> What happens if your method tries to return something that's not an io::Error or a gz::Error? I guess the compiler catches that?
If there is no available conversion to the return error type, compilation fails.
> How would you handle doing this for multiple methods in the same file? Would you rename your enum to DecompError or something to avoid conflicts?
That is an option, although the slightly sad truth is libraries usually have a single big error type and every function returns that.
Convenient fine grained errors in rust remains unsolved, as far as I know. You can do it but it’s a lot of manual work.
maybeError.map_err(...)?https://github.com/golang/go/issues?q=+is%3Aissue+label%3Aer...
They stuck to their guns, took what C did, and made it 1000x better. And in the same sweep made a language that is dead simple to read and code review.
And Go isn't even close to 1000x better than C. It makes almost all of the same mistakes that C did (especially the billion dollar mistake), despite being new enough that it should have learned from them.
Go, like C, is a very get-it-done language. It doesn't let you have any fun at all with abstractions, so you end up just doing your work instead. In spite of its warts, I think Go is remarkable and unique for this quality.
People read code like this and think: its verbose and repetitive.
if err != nil { return nil, err }
But, I almost never write code like that. What I'm usually writing is some formulation of: if err != nil { return nil, fmt.Errorf("Error fetching thing: %v" err) }
Sometimes; you wrap to add additional context. Sometimes; you wrap to get a generic error type into a package-specific error type. Sometimes; you wrap to get, idk, some kind of project-wide HTTP-oriented error type. Error wrapping is the pattern in Go; which is very different from exception-oriented languages.I like this article's syntax for a straightforward "throw error" situation. I wouldn't support its addition, but I wouldn't oppose it either. However, I struggle to imagine a more concise Go-ish syntax I like which supports a "wrap and throw" type situation. Maybe something like:
v1 := Thing1()!
v2 := Thing2() ! fmt.Errorf("Error fetching thing: %v" err)
Phrased in english; the bang operator can follow any statement that resolves to a multi-value function return where the last value is an error type, and the statement is in a function body whose last return value is an error type. If this function returns a non-nil value as its last value; If nothing follows the bang, it bubbles up this non-nill error with no wrapping. If a statement follows the bang, that statement gains an implicit `err` value containing the error value the LHS statement resolved to; and it can return a new error-type which then gets bubbled up.But, again; I don't love or even really like this, its just the best I can come up with. Its not that much shorter than writing it out. Its not obvious how it should behave in the presence of an outer-scoped variable named `err`. Its not obvious how the bubble up should handle the other non-error return values (zero value I guess?)
One thing I rather like about Go is; if you're catching and re-throwing wrapped errors, which is a pattern I like, its actually far more concise than exception oriented languages. The same thing in JS?
let v;
try {
v = Thing()
} catch (err) {
throw new Error(`I died: ${err}`);
}
So; you rarely do that. But in Go, its barely harder to wrap and re-throw than it is to just directly throw; so people do it more often.The downside is that since you don't "need" to catch your exceptions, you don't think about them. That's one thing I really like with Go, errors are in my face all the time, so I have to think about them. Even if I write the infamous
if err != nil {
return err
}
at least I do it consciously. And when I or my colleagues read it 2 months later, we know from reading that this can return an error, and we see clearly how it was handled, so we can think about it and visually see if it was handled correctly.Reviewing and reading code with exceptions is a nightmare, because you have no idea if errors were handled correctly. On every call you have to guess if it throws or not.
Go made something super simple. I write and review a lot of Go code daily, and I don't quite get how these error branches are such a big issue. The code is always very simple to follow through.
So yeah, an alternative for those that be writing C otherwise, and at least Go helps making the world safer even if with a draconian language design.
1) Somewhat similar to what the author of this post achieved, there is a pattern commonly used in Elixir libraries where there are often variants of library functions which end with ! which raise an exception when a problem is encountered.
For example, the file module https://hexdocs.pm/elixir/1.13/File.html#read/1 provides multiple ways to read a file:
File.Read() -> returns {:ok, <<content>>} or {:error, <<reason>>}
File.Read!() -> returns <<content>> or raises an exception
2) Pattern matching is also a common way to deal with this. # This will raise a pattern matching exception if something other than :ok is returned from File.Read
{:ok, content} = File.Read(...)
Pattern matching like this isn't supported in Golang but I think it would be fantastic to be able to write zd, nil := gzip.NewReader()
and have that panic if an error was returned instead.I don't like that I often had to jump up several callers to understand the arguments coming into my function. Go wins here. And I also require performance. While it may have been query abuse by Ecto, the Elixir code base I was in was only able to handle like 3k rps across 5 nodes. I expect nearly 2x that from a single similarly sized Go node making Go 10x more performant in naive implementations. Easier to read and more performant? I chose Go. It also plays nicer with K8s; we had trouble getting nodes linked up in elixir to allow multi node BEAM features.
Similarly I love how in go error plumbing is front and center, at least as important as data plumbing, usually more important.
For some problems, one wants to gloss over things that might go wrong. Don’t use go for those. Go is for when how something fails is more important than how it works.
Bikeshed: How about _two_ bangs?
data := !!io.ReadAll(zd)
The double-bang pattern is unused in Go because it doesn't coerce non-booleans to booleans, so you can appropriate it without stepping on anyone's toes :)The main disadvantage of a Rust-?-like syntax for Go is you lose the ability to add context to the error (e.g. wrapping with `fmt.Errorf("context goes here: %w", err)`). Some people prefer to shove a stack trace inside the error to work around this. I'm not sure what the perfect solution would be. The check-handle proposal adds a special handle keyword to deal with it.
BTW, I am aware of the 'errors are values' concept -- I was the guy interpreting for Rob Pike and the nice Japanese fellow who asked him about it at a conference afterparty and inspired the blog post. I think this pattern is great but it's quite hard to distill it into generic advice ("delay reporting errors until the last possible moment"?). Designing ergonomic APIs for Go can be challenging, and purely anecdotally a lot of the proprietary Go code I've seen at various companies does not do a great job at it.
My #1 wish for Go is that one day Go will figure out sum types and pattern matching. I know that the way interfaces work make this challenging, but I have hope. Using sum types to handle errors makes it much more obvious what the 'right thing' to do is.
[1]: https://go.googlesource.com/proposal/+/master/design/go2draf...
You don't lose the ability to just by that syntax existing. You could still do so by writing it out the long way like you have to do anyway today in Go.
foo().context("oh no")? data := io.ReadAll(zd) ?: return nil, fmt.Errorf(...)
like in Kotlin. I think this likes to encourage overly-long lines though, I'm personally happy with Go's error handling today.Most languages just expect you to read the stack trace and figure it out. Proper error handling tells you explicitly how it failed at each level so you can decide what to do at each level of the codebase.
Having global try/catch (or bang as the author suggests) is easier at first, but makes actually handling error paths worse.
> failed loading config: unable to reach host example.com: tcp: dns lookup: timeout
https://pkg.go.dev/errors#Is and friends can be used to figure out what type/class of error exists in this chain so there is no loss of information either. It's more than just string concatenation. It's an actual tree of errors that also presents well for the logger.
But in practice / production, exceptions / errors are not exceptional. Take a HTTP server. Due to a myriad of reasons, the connection between the server and a client can be severed, triggering an error because the connection was broken. Is that exceptional? No, the internet is unstable and clients are unreliable. Is it therefore valuable to generate a stack trace every time that happens?
One exception is fine, but if you go web scale like Google, the "this connection was interrupted" case happens millions of times a day. Millions of times the cost of generating an exception + stack trace becomes really expensive. An error is cheap in comparison.
We can’t fix the people who cherish their imperfections as a sign of humanity
Ouch. Spoken as a wise manRust struggled with that for about three rounds of verbose error handling, until finally settling on "Result<useful, Error>" and "?". That seems to be about right. C++ exceptions are too much. Writing it all out as in C and Go is too little. The Rust solution is a good midpoint.
Which is saying nothing of Go libraries heavily relying on comment-based programming (err, sorry, we call them "struct tags") to generate boilerplate. The language is successful, but "expense of developer ergonomics" is an understatement.
It seems like a language change would be good; it’s just taking longer than it should.
"Whatever you do, always check your errors!"
Addendum: It is still a good thing to be able to add context to the error message though, so I think you've got a point.
But part of me also sees the Go's team point on this, which is that not all functions always need their error checked - as an obvious example, fmt.Println.
Sure, the compiler could add explicit exceptions for those cases, but that's a very unclean solution and it doesn't handle third party libraries.
The "Go way" is to use the errcheck tool to do that.
And on balance I think that having that defined in a separate tool - which can be configured by the user to exclude modules of their choice, and comes with good defaults - is the correct choice.
I agree, but a lot of other languages handle this much better. In Rust (predictably!) you receive a Result type, and if you don't want to check the error you either .unwrap() it or apply the ? operator to pass it up.
I feel like making "let's not check this error" explicit rather than implicit would be an improvement. Currently in Go it's impossible to tell whether someone forgot to check an error, or if they omitted the check intentionally.
As an aside: I feel like if you are ever in a situation in which fmt.Println returns an error, then whatever situation you are in is already far, far beyond saving. Maybe fmt.Println should just panic on an error, that seems better than output silently being dropped!
Not necessarily, it could just be that the user has closed the stream for one reason or an other. Possibly because they’re running it as a service without having set up an stdout, if the program has useful side effects.
How, exactly, does one find themselves in a situation where they forget to add entire blocks of logic to their application and not notice? We're not exactly talking about subtle bugs here. This is completely missing functionality – something that becomes immediately obvious as soon as testing begins.
Which, I guess, means that the previous developer did no testing at all. In which case, where do you even begin to figure out what else they have forgotten? Such a codebase, no matter the language, may not even be salvageable at that point.
It automates checking but not handling the errors. It's the Go equivalent of an empty catch{} block. It allows a programmer to not care about errors, which works out to the same thing as ignoring them.
Would probably help if you did not need an external linter to remind you. Alas, here as well Go is all hat.
That's true if the function doesn't return a value (other than the error) or if it does and the caller also ignores the return value.
That is, given
func F() error {
...
}
It's legal to call F() without checking the return. However in the case that the function returns a value and an error, like so func G() (int, error) {
...
}
then it's fine to just call G() and ignore anything returned, but i := G()
will cause a compile error. It's possible to assign one or both values to the Blank Identifier, the underscore _, so ignoring the error, while possible, requires the code to reflect the intent, like so i, _ := G()
but i, err := G()
will fail to compile if either i or err are not used following the call.https://go.dev/play/p/Z8xNWiJHPV0
Maybe the compiler should require the error return to be assigned for F(), that's a bit of a quirk that's under discussion.
“Yes” would have done just fine, especially as this is not uncommon when it comes to IO. But then again who’d do IO in Go right?
> will fail to compile if either i or err are not used following the call.
a, err := G()
if err != nil {
panic(err)
}
i, err := G()
i, err = G()
err = F()
fmt.Println("Got", a, i)
> Maybe the compiler should require the error return to be assigned for F(), that's a bit of a quirk that's under discussion.It’s not “a bit of a quirk”. The langage only checks for unused variables which is woefully insufficient and unfit for purpose, and has been since the langage was first released.
I hear this often from Go advocates, but as someone who cut my teeth in Java in 2004, I've only ever seen this done in one codebase: an old streaming parser that I haven't seen since, and where the alternatives were, at the time, generally _more_ confusing than tossing an "unexpected end of input" exception that contained context.
In the 19 years since (14 of which have been my professional career, mostly with Java as part of the job _somewhere_), I've never seen it since.
In the meantime, though, I've run into better, more-explicit error-handling strategies (monadic errors, union types with explicit unwrapping) that manage to make the usually-bad option ("I'm swallowing this error") explicit and intentional, which is not something Go manages to achieve. (Interestingly, these better approaches all pre-date Go, so it's not like there wasn't a better state of the art to learn from.)
But Go also doesn't make error-propagation easy either.
Somehow, Go manages to optimize for accidentally swallowing errors, which is sort of impressively bad. It reminds me of something like INTERCAL: engineered to be as much a footgun as possible. INTERCAL is a parody, though, so it has that going for it.
How so?
I present to you Sneaky Throw, credit to the author in [1]
public class Sneak {
public static RuntimeException sneakyThrow(Throwable t) {
if ( t == null )
throw new NullPointerException("t");
Sneak.<RuntimeException>sneakyThrow0(t);
return null;
}
@SuppressWarnings("unchecked")
private static <T extends Throwable> void sneakyThrow0(Throwable t) throws T {
throw (T)t;
}
}
Now you too can force your callers to accidentally swallow errors and have the code compile ;)[1] https://www.mail-archive.com/javaposse@googlegroups.com/msg0...
The much-maligned checked exceptions, obviously, _require_ you to have a "catch" block for the exceptions in question, or else you get a compiler error.
Option types, Result types, and Either types (which are just generalized Result types) _require_ you to unwrap them explicitly, or else you'll get a compiler error because a Result<T> is not a T.
In Haskell, you've got monadic error-handling inside of do-notation, which is implicit, but at least does the right thing by default of propagating the error back to you, rather than defaulting to swallowing it and moving on.
Meanwhile, in golang, you write this form around 6-7 times in any function of more than a few lines:
result, err := someFunctionCall(input)
if err != nil {
return nil, err
}
...sure, that's so much noise it's hard to miss.....the first time. But since that's literally the only way errors can be handled, you wind up with something more like this: request, err := readHttpRequest(inputStream)
if err != nil {
return nil, err
}
userSubmission, err := parseUserSubmission(request)
if err != nil {
return nil, err
}
err = validateUserSubmission(userSubmission)
userSubmission = formatAndTruncateMessageText(userSubmission)
submissionTimestamp, err := clock.currentTimeMillis()
if err != nil {
return nil, err
}
insertedId, err := saveUserSubmission(userSubmission, submissionTimestamp)
return formatResponse(userSubmission, insertedId, submissionTimestamp)
...how quickly can you spot the error that was swallowed? How quickly could you spot it at 2am when another, downstream service is broken because its submissions are failing validation but the validation error isn't propagated?By taking away the typesafety of requiring some sort of type wrapper for multiple return that must be unwrapped, _and_ by taking away the enforcement that you have to check for and either propagate or explicitly swallow the error (by way of a result type or even checked exceptions), Golang takes away your guardrails, leaving you on the mountainside and liable to fall off easily.
By making you do repetitive boilerplate "if err != nil { return nil, err }" every other line or so (rather than providing automatic error-propagation machinery like Haskell's do-notation or Rust's `?` operator), Golang lulls you into "highway hypnosis"[1], setting you up to be much more likely to accidentally drive over the cliff. It makes the Right Thing™ tedious, easily omitted, and only enforced by your own constant vigilance (or complex external tooling that has to guess at your intent), and makes the Wrong Thing™ the default.
I've actually never ever seen that done in production code in the last 20 years I've been programming. I've only seen exceptions used for errors in which case the described behaviour is exactly what I want.
The fact that Go advocates have to exaggerate the issues with exceptions to make the design of errors in Go seem reasonable makes me extremely suspicious.
And yet, Golang supports Goto.
Also, just because your (not talking to you, just a pet-peeve of mine) CS101 professor said that Goto's are bad, doesn't mean it is true in 100% of cases.
1. it's very very tedious to have two unrelated error systems in one language (yours and the one everyone else in C# uses) 1. as Go demonstrates, this type of error handling is extremely tedious even by itself
But is the tedium worth it?
But, exactly like you say, you end up fighting libraries - particularly the standard libraries, and it ain’t a fight you will win.
In typescript it actually kind of works - better at least. JS APIs often are errors-as-values already because of the old continuation async APIs, and typescript of course lends itself incredibly well to rich return types.
To me part of a language's utility is measured by how well it works in the group setting. That's what typically drives the industry, and more to the point for me it is the situation I will encounter most of the time.
To everyone who complains about the "if err != nil { return err }", honestly, you are doing it wrong or you have a toy application. I have written several, large, high scale, highly available systems processing multiple billions of requests a day. When analyizing those code bases, empty error returns like that accounted for 4% or less of our code. We always were doing _something_ with the error. Metrics, logging, reties, sending off to a different workstream, etc.
You could even have `catch` capture only the last value in the result tuple and it would still be big improvement.
"Point on the doll where the exceptions hurt you." I worked on a project with exceptions as control flow (cough, twisted python, cough), and the error handling was caught several classes and mixins up and over in the file directory. In some far away file, your exception triggered a callback or an errback, and if that excepted, similar magic happened. It got to the point that several engineering choices had to be made around "well, this really would be nice to have an exception and some standard handling, but the framework will take it and do strange things."
I _love_ handling my errors where they are created.
value, err := foo();
if err != nil {
// handle err
};
value := foo() catch err {
// handle err
}Although the keywords panic and recover were chosen instead, this functionality has existed since day one.
But we're talking about errors here, not exceptions. They are very different things.
Well, in some languages it's the same and I have yet to come to a practical problem with it being the same.
The discussion is always just about the pleasantness of the development experience, and exception handlers are simply not pleasant to use (when carrying errors) – to the point that, when using those languages, developers go out of their way to find ways to avoid having to deal with errors to not have to write out the monstrosity that is to catch an error thrown.
Indeed, that's life. You have to deal with the hand you are dealt. If that means changing how the application functions to not make your developer life a complete living hell due to quirks of a programming language, so be it. But idealistically, that does not make for a good language design. There is a reason why they are called exception handlers, not stack traversers, or whatever.
Indeed, there is a class of programming, known as scripting, where errors actually don't matter. If something bad happens you simply fail, deal with the problem through human intervention, and try again later. It is not unreasonable to carry errors up the stack using exception handlers in this type of problem domain. Arguably communicating an error state this way is the best solution we know of to that problem.
However, there is another class of programming, known as systems, where error handling is the most important code you will write. This is the domain where exception handlers are just plain not suited to the problem. The idiomatic Go solution is not pleasant, but a huge improvement over dealing with catching exceptions everywhere.
As before, because catching exceptions is so painful, many applications that probably should be systems are pushed into being scripts. Which may very well be a good engineering solution to the problem of dealing with a quirky language, but idealistically a programming language will not shape your application requirements like that.
But, yes, Go is not at all designed to be a scripting language. It has been quite explicit about that since day one.
Maybe you're leaving out the stack trace that one usually expects when propagating exceptions? I'm not sure that is meaningfully different, though.
I think it is implied that one should not return both a value and an error. Is this true? Is there much code in go that returns both and lets you decide if you want to take the value and carry on, or take the error and stop?
Is there much idiomatic code where a function returns a tuple of actual values? For example a function in some shipping code that returns the largest dimension of a package to be shipped where it can only be of a maximum size or a maximum weight? Does this kind of idiomatic code also rely on the contract that only one value will be set?
Does go have the typing facility to have a union type for both a value and an error, and does anyone use this in preference of returning tuples? If types in go can be null then this still allows for ambiguity and it would surely be unpopular to produce code that breaks the value/error tuple pattern, but I assume some left field hackers are breaking the rules somewhere.
See some bikeshedding and ongoing discussion [here](https://github.com/golang/go/issues/57644).
Note that there is kind of a philosophical cul-de-sac here. If you understand and expect your "errors" (= exceptional situations that prevent the program to do what we thought the user wanted) you can handle them, and they become part of your program normal logic.
If you don't understand or ainticipate your errors, or choose to neglect situations that are not really rare or are really dangerous, no amount of special language constructs will save your users.
https://go.googlesource.com/proposal/+/master/design/go2draf...
Just to latch onto this remark, are they actually exceptional? Say you have a REST API, does something with a database. 1 in 1000 requests fails, so it's exceptional I suppose. But then you go web scale, and your application gets called a million times per minute. Suddenly you're dealing with 1000 errors per minute; not so exceptional anymore.
Besides, errors can also be things like 'SQL row not found', which are expected in the normal run of things.
zd, err := gzip.NewReader(fd)
to return an error rather than waiting for an actual read. The stdlib is usually pretty good about this but third-party libraries are rife with this kind of misdesign.---
0: https://github.com/golang/go/blob/go1.21.3/src/compress/gzip...
1: https://github.com/golang/go/blob/go1.21.3/src/compress/gzip...
2: https://github.com/golang/go/blob/go1.21.3/src/compress/gzip...
Is the author just telling us about a tool they privately made but are not sharing?
And any code you write with this "bango" operator is incompatible in anyone else's Go environment unless they have the author's same tool configured to preprocess their code?
Otherwise it would be an interesting idea but also more or less what Lombok is for Java.
func decomp(filename string) ([]byte, error) {
fd, err := os.Open(filename) `err`
defer fd.Close()
zd, err := gzip.NewReader(fd) `err`
data, err := io.ReadAll(zd) `errw:"your device could be broken: %w"`
return data, nil
}
Where `err` tag just forwards error if nonnil, and errw is similar to Errorf.I think instead of a bang it would be better to have a keyword like this.. similar to defer to defer a function. Maybe `try` to give a function a try and if any error is returned then bubble up.
defer cleanup()
bar := try somethingelse() // equal to bar, err := somethingelse() .. return err
Returning an error and a value as a product type with an implicit promise to ignore the value part if the error part has a specific value is not the right thing.
There's a credible chance sum types are the right thing in a dynamically typed language too but I haven't settled on exactly what what should look like yet. It's not a thing in the dynamic languages I know of.
P.S. That said, any program written in Go is an absolute shitshow of crappy UX because of the (inevitably) inconsistent and often incorrect error handling.
And even then, because Go uses insane and outdated OOP practices like casting all pointers to a generic base class, even reading the source code is a exercise in frustration and rage.
It is silly how many Java or Python programs display stacktraces on trivially preventable problems that are not bugs (e.g. file not found) instead of giving short human readable messages.
Thank God I don't have to code anything in Go myself.
The issue is that application code usually has several layers between the end-user and the fundamental operations like Read()/Write(). Bookkeeping errors through all these layers is a chore. You can skip the layers but then you get spaghetti.
In what world is this cleaner or easier to read?
Go is designed to be easy to read and easy to write. err != nil is not hard to read. If you don't like the repetition, it simply isn't the language for you. I'm perfectly happy with how Go does it. I do not want the language morphing into another Rust.
``` fd, .. := os.Open(filename) ``
zd, _ := gzip.NewReader()
?
This too is obvious to go readers, and the current correct way to ignore errors.What you want is to send the error back up, without the need to be super verbose with
if err != nil {
return nil, err
}With this distinction in mind, the entire article boils down to two points:
1. The author is basically complaining about having to type more (fair, but not interesting)
2. The author proposes that appending a "!" to a statement is somehow clearer than explicitly returning an error value (absurd and eyebrow-raising)
The whole article then becomes "I like Rust's syntax better".
What sounds absurd to me is obscuring the actually interesting control flow significantly in favor of either showing the trivial control flow or alleviating the need to learn the one (1) single operator that represents said trivial control flow.