Declined Proposal: A built-in Go error check function, “try”
github.com
github.com
It's a language intended to have few primitives with an emphasis on code being transparent and errors being values, requiring you to think about what they might be at each point as you're forced to carry them up the chain.
Do I particularly like managing errors that way? No, but I do think that it improves the transparency and quality of a lot of Go projects.
The same goes with the Go ecosystem and tools. You might not like how gofmt forces your code or the documentation conventions used by golint but the fact that we all use the same convention and documentation is awesome and it's what allows things like godoc.org. I think the proverb is "gofmt's style is no one's favorite, yet gofmt is everyone's favorite".
And things like the lack of abstractions and generics are what create a community that's less reliant on dependencies. "A little copying is better than a little dependency" and you can see it in stark contrast to something like the JS community with its require('left-pad') NPM ecosystem.
So, yeah, I like that they didn't fragment the community around something as arbitrary as this. And I get that some people won't like it, just as they don't like the lack of generics, but there is strength in some of these approaches that isn't immediately obvious.
Though with generics we could have the error values be less invasive. Learning the Scala mindset was eye opening. For e.g. the design patterns of an Optional[T]’s primary code flow are the same as a Try[T,E].
With common transformation idioms, you learn to recognize code patterns the same way one might recognize data structures. It’s no less magical and it simplifies greatly.
I don't see this as an either/or situation. There's a continuum between do whatever the hell you want C and rigorously defined Idris code. Picking a spot on that line involves many trade-offs and everybody has to choose what's right for their situation. That means Go approach is sometimes superior to the Scala approach for a certain class of problems in certain domains.
Edit: don't downvote me, tell me how I'm wrong.
Being close c means you discover errors at run time, then you have to come back to cover them.
Would be nice if there was a language that scaled from C to Idris gracefully, but C++ and Scala make me think that's a pipe dream.
So long as we can all agree that it feels super bad, I guess this is fine. But it does sort of mean that Golang approaches the Java world back with checked exceptions where principle trumped ergonomics.
That lead to a world where folks felt "forced" to use Java, and that's a sticky label that's difficult to remove. I think the Golang community is going to find itself growing more slowly as folks increasingly realize other options offer similar benefits without the bad ergonomics.
Personally I enjoy writing Go code similar to the way that I enjoy writing Python code. So they did something right because I wouldn't ever say the same about Java or PHP.
Edit: As a fun tangent... Python experienced the same kind of fracturing that adding generics or other dramatic features to a language can cause with the 2to3 move. There are still projects based on 2.7. It was hard to get people all on the same page once the community was split. What's also interesting is to watch the same thing happen with asyncio, which is practically an all-or-nothing thing to use in your Python project, so much so that most Python code still doesn't use it. Just an observation, but breaking changes to a language can really do a number on the community.
But the asyncio example is probably a better example of a fundamental program design change that's causing weird fracturing and incompatible library designs. Trying to use non-async from an async framework is a mess, trying to use async from a non-async is hard to integrate, etc.
There is fracturing with asyncio. Look at popular libraries. The "requests" library comes to mind, but there are others. It's hard to use different 3rd party components between the two paradigms which causes fragmentation in the libraries. You end up with aiohttp-requests and stuff.
PS: it does make a huge difference. If someone posts "how do I make a web server?" on a forum you'll get two different frameworks and approaches as answers. The community basically goes in two directions and much of the work becomes incompatible. The multithreaded sync vs event loop async just don't play well with each other. There still aren't many 3rd party libraries that really focus on asyncio because much of the community is still in the "threaded" design.
The language itself is fairly clean and very pragmatic.
However I think he came by his dislike honestly. When he was actively working, Java was really quite frustrating.
Today each language ecosystem is much more self sustaining and has its own bizarre culture that looks frustrating to outsiders.
This is partly why I like Golang so much, they’ve been focusing on readability.
I will agree that Lambdas are better than anonymous inner classes.
Speaking as someone who's just returned to coding after a two-decade absence (mostly for fun), this is the #1 thing I love about Golang. I can actually read code from very experienced and skilled developers and understand what they'd written. And yet the language itself is highly capable - I don't feel like I'm learning some "beginner" language, but a "real" one that I can use for practical purposes.
For that comparison to work, java would have needed to only have checked exceptions, with some added language features to deal with them cleanly, and then it might actually have been a viable design. Littering your code with try-catch is what makes exceptions infeasible for error handling.
Should I compare it to "Golang Dependency Hell Claimed My Project?"
> For that comparison to work, java would have needed to only have checked exceptions, with some added language features to deal with them cleanly, and then it might actually have been a viable design.
Why? That's not what happened with Golang's error handling?
As someone just learning Go, what is Golang Dependency Hell?
also google: we don't have that problem at google, so it's not a problem
Of course you can. But it would be hilarious considering Java Module system (Jigsaw) which took decade in making still does not support versioning. People are left to use bloated crap like Maven or Gradle. But since these products are called "enterprise grade" developers are not supposed to call them crap that they really are.
What exactly is "bloated crap" about maven? It works exactly as advertised and a heck of a lot better than go modules/vgo.
If you say XML you lose (it is a config fmt get over yourself...). If you say it downloads the world I suggest you look at the output scroll by on any decent sized project in Go land.
So enlighten us all please with your wisdom.
"Besides that Mrs. Lincoln, how was the play?"
I honestly don't see the big difference between littering your code with if/else blocks versus littering them with try/catch blocks. Can you elaborate?
Checked Exceptions in Java, for example, force you to litter your code with catch blocks and error handling code that doesn't actually recover from the error. It transforms it. It propagates it. Languages like Go with explicit error handling do the same thing. I don't think that's bad in all situations but often it comes down to what one thinks is an unexpected error for a given project.
Nothing about try/catch is like a "roaming goto".
I could live with a runtime exception, or perhaps an API that differentiates parsing "potentially not-parseable JSON" and "JSON I can vouch for".
void foobar() rethrows IOException as AppException rethrows FooError, BarError as WtfError {
Being able to express that a bunch of internal exceptions should be wrapped in an application exception would save a ton of boilerplate.The exception syntax itself is also clunky. If you have a complex expression, you have to extract it and assign it to a variable. But you still have to declare that variable outside the try/catch. And, of course, every exception requires Yet Another Class Definition. I'd do:
class SomeClass {
exception ThingBroke handles ArithmeticException;
exception DatabaseDown handles SQLException;
void method(int a) {
try {
int x = (555 / a rethrows ThingBroke) + (getNum() rethrows DbDown);
} catch(ThingBroke e) {
I think the other issue is the separate hierarchy for unchecked exceptions. In theory, they were supposed to be issues you couldn't recover from, thus the correct response was to let the caller fail. But now UncheckedIOException is not a subclass of IOException.Maybe a better approach would be to add an Unchecked interface, though from my reading of the source for exceptions, they seem to be very brittle and maybe there's just no fixing it.
It would but I think it's the wrong approach -- you're actively changing the error information that really provides no additional value except to make the type-checker happy because the alternative is too verbose.
Wrapping does have value if you are really adding useful information but the example here is just changing the type which really doesn't add anything useful.
And even C++, after dropping exception specifications in C++17, might get a simplified version back in C++23 as part of value based exceptions proposal.
Actually, I don't think everyone agrees it feels super bad. I personally like having all of my error handling be explicit, painfully explicit even.
>approaches the Java world back with checked exceptions where principle trumped ergonomics.
I also have to disagree here. To me, checked exceptions are the worst of both worlds. Here you have additional language features, but nearly the same verbosity. Your 'happy path' must be surrounded by try blocks. Worse, everything that happens in a try block is 'flattened.' Not only does this mean you may need many try blocks, sometimes it can even be difficult to take individual statements and put them in one try block. For example:
try {
doThing(doOtherThing());
} catch(...) {
// What happens if doThing and doOtherThing throw the same exception? Do I have to use a temporary variable?
}
Also, exceptions have been overloaded to handle everything, including runtime errors. I think this is more subjective but I strongly dislike it. I do think runtime errors should be possible to handle, just ideally through a separate, more explicit paradigm. try {
doThing(blah[0]);
} catch(...) {
...
} catch(IndexOutOfRange) {
// Handling runtime errors at the same level as application-level errors!!
}
Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them. Like: result, err := doThing(); // Error
return result
result, _ := doThing(); // NOT an error
return result
I think this is excellent. It may lead to complaints about an annoying compiler, but most importantly it leads to fewer mistakes. You can still explicitly tell the compiler to shut up, but it is obvious.Language ergonomics are complicated, but the benefits of Go's approach are hard to deny. Unfortunately, nothing comes without downsides, and it seems like solving the error handling ergonomics issue is a tough one. I think repetitiveness aside, the Go error handling ergonomics are great, and that's exactly why they attempted to reduce repetitiveness. But, in reflection, a lot of that repetitiveness can also be reduced by refactoring your code, so it may not even be quite as bad as it seems.
Go has exactly the same issue. But with exceptions at least you can group multiple statements together and handle them with one catch block. With Go you have to use multiple if blocks to get the same semantics.
> Go doesn't even really force you to check your errors, it just makes it harder to accidentally not check them.
Go doesn't make it harder to accidentally not check errors. If you call a function that only returns an error, such as os.Mkdir(), then the compiler will not warn you when you forget to handle the error.
> Language ergonomics are complicated, but the benefits of Go's approach are hard to deny.
I don't really see any benefit to Go's approach over exceptions or result types. If making it obvious that errors are handled is important, there's a solution for that that's much more elegant than if-err-nil blocks everywhere. It's precisely the solution that the Go community just rejected.
Go does not suffer from this issue at all. I am not talking about flattening from the call hierarchy, I am talking about flattening the try scope itself.
(In case it isn’t obvious: in Go you’d be forced to separate two error checks. But it results in still less cumbersome code, since you only need scoping for the error handling portions.)
But forgetting that, because passing errors down in Go is explicit, it is actually customary to use error wrapping to add context as an error is propagated, which does allow for more precise error handling actually.
>Go doesn't make it harder to accidentally not check errors. If you call a function that only returns an error, such as os.Mkdir(), then the compiler will not warn you when you forget to handle the error.
Go vet will do that. It is a good 'first step' to configure when setting up your CI/CD (many will do it by default.) Go vet's 'unusedresult' checker does this.
>I don't really see any benefit to Go's approach over exceptions or result types.
It is a simpler language than Java. You get most of the benefits of checked exceptions without all of the calories from exceptions.
There isn't a meaningful difference in cumbersomeness between having two try-catch blocks and two if-err blocks. There is a meaningful difference in cumbersomeness between what Go has today and "try foo(try bar())". Which is why it's so unfortunate that the community killed the try proposal.
> But forgetting that, because passing errors down in Go is explicit, it is actually customary to use error wrapping to add context as an error is propagated,
People say this, but in practice any code search reveals that "if err != nil { return err }" is everywhere. I believe that many Go projects aspire to annotate all errors, but much fewer actually do.
Ironically, the nice thing about exceptions, as well as Rust error chaining crates, is that they do this automatically, so in practice programs written in languages with those features tend to have better error diagnostics than Go programs do. Computers are better at doing things consistently than humans are.
> It is a simpler language than Java. You get most of the benefits of checked exceptions without all of the calories from exceptions.
Go is a more complex language than Java is overall, because of all the special cases the language adds to magic types like maps and errors that are just part of the library in Java.
Code searches in languages with exceptions tend to wrap the tryblocks around massive portions of code instead of the individual function calls to the point that you have a top level doing:
try:
...program here...
except:
print('¯\_(ツ)_/¯')I wasn't saying that Go's approach solves this, just that it's not a problem unique to Go.
And in the case of Go it's painfully obvious that you're ignoring all of those errors whereas in other languages you can't always tell, visually, that they're being ignored because of the magic of exceptions.
Almost all actual error handling in code is either error translation and re-throw, resource cleanup, or automatic retries (sometimes you retry the same request, sometimes you try a fallback option, but it's still the same idea).
The user however may be able to check and actually fix their internet connection, they may fix the typo they did in the config file, they may call support to see what's happening to the database etc. - your program can't do any of these things.
That's why exceptions work so well in most languages, especially GC languages where you have dramatically fewer resources to cleanup: they bubble up automatically towards the initial caller, which is often the user. Threading messes with this, but if you use the more modern async style (async/await in most languages) you get proper exception propagation even then.
But there are many people including some well-known and highly-respect people who believe that exception handling in fact does not actually work well:
- https://www.joelonsoftware.com/2003/10/13/13/ - https://blogs.msdn.microsoft.com/larryosterman/2004/09/10/st... - https://www.atlassian.com/blog/archives/exceptions_are_bad - https://stackoverflow.com/a/1736320/102699 - http://xahlee.info/comp/why_i_hate_exceptions.html - http://www.lighterra.com/papers/exceptionsharmful/
#justsaying
Object thing1;
try {
thing1 = doStuff();
} catch(SameException e) {
// handle error 1
}
try {
return doOtherStuff(thing1);
} catch(SameException e) {
// handle error 2
}
vs thing1, err := doStuff()
if err != nil {
// Handle error
}
thing2, err := doOtherStuff(thing1)
if err != nil {
// Handle error
}
return thing2
Happy path is flat. Control flow is obvious and simple. I have not much more to add.>People say this, but in practice any code search reveals that "if err != nil { return err }" is everywhere. I believe that many Go projects aspire to annotate all errors, but much fewer actually do.
https://github.com/search?l=Go&q=%22errors.Wrap%22&type=Code
>Ironically, the nice thing about exceptions, as well as Rust error chaining crates, is that they do this automatically, so in practice programs written in languages with those features tend to have better error diagnostics than Go programs do.
Rust is a different ball game. Rust does not try to be simple. It comes at its own costs. (I like Rust too.)
>Computers are better at doing things consistently than humans are.
These empty platitudes come up frequently when debating language decisions online. But, it's so meaningless in so many dimensions. I mean, we could also 'use the computer' by adding C macros on top of Go and use them to reduce repetitiveness, but I don't think many people will applaud you for it. Simply applying computer code to solve a problem does not constitute good design.
Go's proof is in the pudding. It's been extremely reliable for me in real world applications.
doOtherStuff(doStuff())
is the happy path, but it's been hidden among lines of noise that do nothing more than return to the callers who know what to do. Generating this using cpp or m4 would suck, but it's still better than not generating it due to wasted effort (especially re-reading) and mistakes. Stuff foo() throws SameException {
return doOtherStuff(doStuff())
}
Whereas in Go the exact same function must be written like this: func foo() (Stuff, SameException) {
thing1, err := doStuff()
if err != nil {
return err
}
thing2, err := doOtherStuff(thing1)
if err != nil {
return err
}
}
Propagating is by far the most common "error handling" and Go constantly makes you break the flow of the code to do it.(also why debuggers still insist on line-based breakpoints is beyond me, why can't i right click at a call and put breakpoint at the call itself instead of the line where the call lies on?)
Stuff thing1 = doStuff()
return doOtherStuff(thung1)
It's still easier to read without the explicit error handling.And maybe it's because I've developed a very consistent pattern, and when I find that there are too many errors to handle in a function it helps me realize the function is probably doing too much and needs to be split into multiple functions.
IMO it is all about training your brain and being accepting enough to work in the dialect of the land, as opposed to demanding to speak English when living in France. :-)
Not if you're doing it right, which means bubbling up (read: adding the exceptions to the "throws" clause) exceptions that you can't handle _then and there_. You leave the "real" exception handling to the code that's closest to the end user and can actually handle the error in a meaningful way.
For example, in this pattern when you implement the business logic in a REST service, generally there's no catching at all, instead the exceptions are declared as rethrown and way up the stack you would have a global catch all that serializes the errors into JSON and sends them to the user. Simple. In Go you're forced to do "if err{}" checks everywhere, particularly in your endpoint's business logic. It's actually _way less_ verbose and burdensome in Java if you do exceptions the right way.
I dislike it because it does the opposite, it makes it too easy to accidentally continue execution when there is an error:
doThing(); // Error
doOtherThing();
Which isn't possible with Exceptions.The only thing that would signal that error handling is missing is the absence of boilerplate to handle it, which is an ugly UX for a language to rely on esp given there's no indication from scanning code that all methods that return errors are handled.
But I agree, the fact that Go allows this is bad, imo. It would be better if you had to explicitly suppress errors, even with something like this "_ = doThing()" just to make it harder to miss.
How often do you handle or explicitly ignore the error returned by fmt.Print? This is just proof that golang error handling is error prone.
result, err := doThing()
if err != nil {
return nil, err
}
result1, err := doSecondThing(result)
return result1, nil
Will not trigger a compile time error.With the amount of boilerplate in go that is simply about bubbling up error codes, it really doesn't seem that much cleaner than Java exceptions. An if block just as many lines as a catch block but again, you lose the typing.
And the more I think about it, the more I think well written Java is cleaner than well written go simply because the throws keyword leads to strictly less boilerplate. I feel like this stigma is simply about how much bad Java exists (and there's a lot). I can't help but feel like a WebSphere written in Go would be just as ugly as its current Java incarnation.
Quite the opposite. You have to explicitly ignore exceptions, whereas it’s easy to accidentally miss an error in Go. For example, how many times have you seen this?
defer f.Close()
Close() returns an error; this code ignores it. defer func() {
err2 := f.Close()
if err == nil {
err = err2
}
}()
Not perfect, but better than nothing. I could use Wrapf to add information is err is already set. :-pThere's no amount of downvote that will change that fact.
Exceptions feel super good, even as you're taking too many shortcuts and glossing over things. Proper handling of the unhappy paths is often going to feel like a slog, because it often is complicated, and it's often a slog. Glossing over error handling in golang can feel bad exactly when it should.
I'm not sure I follow this talk about "short cuts.". Exceptions provide stronger guarantees by default than multiple-value-bind and manual if statements. Your code fails closed as opposed to open.
> Proper handling of the unhappy paths is often going to feel like a slog, because it often is complicated, and it's often a slog.
I'm used to languages where this generally isn't the case, but sure. Why twist the knife in the wound though?
> Glossing over error handling in golang can feel bad exactly when it should.
When you capture but fail to check an errval and an untraceable default value starts rattling around inside your code causing unexpected behavior? It's like an NPE but worse because it won't immediately trap. It's more like a double free.
I've seen that happen more than a few times. Some of it in new production code.
Definitely. The question is, are the benefits in those contexts worth the cost of the potential abuses? As a codebase and company get larger and larger, the higher the probability that a problem will get into the codebase.
There is an analogy here with C++ templates and with method_missing meta-programming. There are some contexts where they make things a lot easier. However, in a large codebase, the probability that some programmer, somewhere is going to cause a costly problem with a bit of nifty overreach goes up and up.
As they say, an ounce of prevention...
Any error that I have forgotten to handle is a bug, in the sense that it was a bug to have forgotten to handle the error in the first place. So the standard behavior of "treat every unhandled exception as a 500 response" is technically correct there. Upon consideration, some errors are actually so rare and/or catastrophic that I don't even have to worry about handling them because I wouldn't do anything different anyway. For example, suppose I have a database-backed service that has no meaningful failover if the database is down, and it throws an exception whenever the database is down. There is no reason for me to have to handle that error.
There are other error cases where I have to stop handling the request but it isn't my fault. So if my service has an endpoint that expects a JSON POST body and the body doesn't even parse as JSON, maybe I need to handle that so it throws a 400 instead of a 500. But I could just wrap that exception with a custom class that my error handler generates a 400 response for, and then the rest of my code could just go on assuming that the JSON did parse and that it was valid.
Probably the closest equivalent would be if you had a forking web server where each request had its own thread, and you could effectively kill the thread with a response object at any time. It's not really a case of "using exceptions for flow control" or whatever; it's more a case of "this entire unit of execution is finished and can kill itself just as soon as it's done leaving a note".
You didn't have to because there is an precise, robust non-checked exception handling system which takes care of that: the hardware catches the situation, dispatches a handler in the operating system which fixes it and re-starts your program at the original machine instruction to try the memory access again.
Because I can't stand Golang error handling. It's repetitive, it's error prone, and other language features interact with it so that when you make a mistake it can be as hard as a double free to track down where the erroneous default value was introduced.
On the other hand, using Rust, Ocaml, F# or Haskell I understand how my code composed and I can be confident I can just use an error handling strategy. The only complexities appear, as with everyone else, when we have asynchronous code.
So I don't mean to disagree with your feelings, but they're sure not mine and they're part of why I don't use Golang. I was excited about try() because it at least addressed the most tedious part.
Concise languages can be more challenging to read because you have to understand more about each symbol/word in the language. But verbose languages can be more challenging to comprehend because there's a lot of symbols which don't signify anything.
I suppose concise / implicit is fine when the thing that's being hidden can't go wrong, like in:
[i * 2 for i in 1...10]
The loop counter increment logic can't possibly go wrong, so it's fine to not think about it.
Regarding error-handling, don't you want to think about? If you're calling a() followed by b(), what should you do if a() fails? In some cases, b() shouldn't be called, but in others, it should, like deleting a temp file. And if you have to think about it, it's better to be explicit?
To put it more simply: yes, the developer should have to think about what they do in the case of an error. And then the amount of work they do -- and the code produced, and thus the work reviewers have to do -- should be proportionate to how unusual the necessary error handling is. When I see explicit error handling, that signals to me "hey, this is an important case that we need to handle in a particular way".
Great comment. I would add how unusual _and critical_.
One of the things I love about Python is that while I know errors can occur on practically every statement written, I only have to add error handling for likely / expected / critical errors. Any unlikely errors that occur, even in production, will show a detailed stack trace (lots of context), making them easy to fix.
In my experience, things work as expected 98% of the time. For some software, like a pacemaker, checking the execution of every single line of code and even having redundant error checking is not overkill. For other software, like the backup software I work on, having one customer out of 1000 get a weird error is something I'd rather deal with as a support ticket rather than having to anticipate it while writing code.
Of course error handling is important, but requiring 3 lines of error handling for every 1 line of actual code has kept me from investigating Go to replace Python for HashBackup. I'd love to get the performance increase, but not for a 4x expansion of LOC.
Or try Rust. ^.^
In this case, I don't think anyone is arguing that error handling should be implicit. They're saying there should be an explicit way of saying "handle this error in the common way." This actually makes the distinction between common and uncommon cases more explicit, because their differences aren't buried in boilerplate.
It's similar in Go since you can't compile with unused variable so you need to explicitly discard the error with _. Ex: result, _ := func() This is for multi-value returns, for single value you can even omit the _
Your example re Go errors is incorrect. The go compiler allows you to ignore errors in returns without any compiler error.
For example
err := doThingThatErrs()
and
doThingThatErrs()
are both valid Go code.
Try Return [Bla(), null] Catch err Return [null, err] End
I do like this syntax better, bc the different scopes cause a lot of nesting
But yeah, Go dev do not see VB's features as being "features."
VB's biggest problem IMO is that it tried to compete with C# instead of maintaining its original raison d'être which was to be a highly productive tool that required very little programming skill to be able to start building real solutions for business use.
But I digress...
Go's error handling isn't any more error prone that writing if statements for all the rest of your code. if err != nil is no different than if age < 18. Either one is a branch in the code. I can literally count on one hand the number of times in 6 years of full time go development that I've seen people miss writing the if err != nil.
Being explicit is good. Spreading out the logic is good. Cramming a lot of logic into one line is bad.... and that's the sole purpose of try.
Maybe there's another way to make error handling better in Go. I'm not averse to looking into that. But try wasn't it.
You're talking about writing if err != nil being tedious, but what about matching Results from Rust, isn't that tedious? What about writing proper catch blocks in java or C++ or python, isn't that tedious? It's all just logic.
I absolutely agree. Beyond the human parsing aspect it also makes commit changes easier to reason about and review. I want functionality to be limited per-line and view the ability to combine a lot of functionality into one line as a liability more than a benefit.
Go's error handling isn't carefree or hands-off, but that's because error handling is serious. Especially in network code and cryptography.
That said, I am not a huge fan of fluent interfaces. I much prefer passing in a struct as an "args" parameter, in most case (but not all.)
If you only need to propagate the error or cleanup resources then propagate the error, then all you would write is... Nothing. And cleanup+propagation is by far the most common error handling strategy. In Java and Python exceptions even add context for you automatically to help track down what happened.
> you only need to write [error checking] when you actually have something meaningful to do.
Although it's the subject of a lot of ridicule, `if err != nil { return err }` is actually bad Go code, and not often written by good Go programmers. Errors in Go are, at a minimum, annotated with contextual information before being returned. Frequently, they are programmed-with in other, more sophisticated ways, depending on the domain of the program.
Shifting your mindset to understand errors as something significantly more important than the off-gassing of your program's execution is worthwhile in general, and, in Go, fundamental.
What surprised me when I last wrote Go was that there was no out-of-the-box solution to adding a stack trace to the error.
This is all to say: I can understand why they aren't more naturally part of errors, and I think it also helps explain why they are part of panics.
And with all that said, I wouldn't object to making stack traces easier to add to errors, as long as it was opt-in.
Like the people who wrote the Go stdlib? Because that's filled with those - just look at the net/* packages.
Many programs can benefit from a more structured approach to error management. But once you get past the minimum (above) there's no one-size solution for what "a more structured approach" looks like. I really enjoy how upspin.io does their errors package, though it is somewhat esoteric. I'm also reading and generally liking how Cockroach does things, though I don't like the coupling to Protobufs.
So using these functions is becoming part of what good Go programmers do.
I would much rather have an error with too much or duplicate annotation than one with not enough. And I would further argue that, yes, in the stdlib, errors are generally under-annotated.
err = doThing()
if err! = nil {
return errors.New("Error doing thing", err)
}
This doesn't add any useful information for whoever is reading the code, it's just boilerplate that you learn to skip while reviewing, while hopefully not missing any important thing that does happen on the error path.As for 'programming with the error', I would like to see an actual use case for constantly doing this, and why exceptions would prevent that pattern. The only one I can remember is something highlighted as a 'good practice' by Rob himself: write everything you want to a bufio.Writer, without checking the error messages, and then calling Flush and only then checking if maybe something failed. If this is good, safe, sad-path-first, errors-are-values style... then my taste in programming is obviously bad. Obviously, the same could be achieved with exceptions.
If above is true, then how about:
err := renderTemplate()
if err! = nil {
return fmt.Errorf("Error rendering template: %s", err)
}
The end error could then be something for example: Error rendering template: Compiler has failed: Cannot load template: File /tmp/test.tpl was not found
------------------------ ------------------- -------------------- --------------------------------
| | | |
Returned by that | | |
example Returned by the | |
fictional compiler Returned by the |
fictional template Returned by the fictional file
Loader reader
I didn't even twist your example, and yet you can already see more information. And with that information, even a user can understand what's going on clearly. So ... more useful?Regarding you example: the code itself still contains redundant information for someone reading it. True, the error ends up being nicer, though I would argue that the user would have been better served with a simple 'failed to load template file: /tmp/test.tpl', no need to show the pseudo call stack (so, only the fictional template loader should have been wrapping the error,for this particular case). And for a developer, the full call stack may be more useful. Exceptions would give you both for free - a nice message that can be shared to the user by whoever caused the most understandable error, and a call stack that can be logged at the upper layer so developers can see it if a bug is logged, and get a much fuller context.
But then I am super zealous about making sure my error messages understandable without the need to track down other information. For example, I want to know that (something like) "the config file needs group read permissions" not "file cannot be opened." But maybe others value ease of programming more than I do and are less concerned about error UX than I am?
Using Rust, which makes errors explicit like Go, has been eye-opening to me. My programs never crash because I've handled every error condition. No effort on my part. No tests needed.
By the way, Go is much happier to crash than Java - for example, a simple array index out of range will cause a program crash in a typical Go program, where it would only cause a request failure in a typical Java program. Not sure how Rust handles this.
Finally, choose that isn't tested (manually or automatically) is very unlikely to work. Maybe you can guarantee it doesn't crash, which is a much weaker guarantee, but I doubt even fully proven code (like seL4) is all bug-free before ever being run.
Rust handles your out of range scenario the same way Go does.
If any of this matters to you, the good news is that Kotlin's sealed classes (and soon, Java's sealed classes) allow you to easily implement your own Result-like sum type.
I understand how Result forces you to handle the possibility of an error when you want to access the actual return value, but I don't know what happens if you aren't planning on accessing the return.
I'm also curious how Result-based error handling composes. For example, if I want to sort a list of structs where my comparison function may fail, can I use the built-in sort function, and still get any error that may have occurred back?
With (unchecked) exceptions, this is trivially easy - the sort() function doesn't need to be aware of exceptions in order for them to be propagated through it. With checked exceptions in Java, you need to go through a little dance of wrapping and unwrapping, but it can still be done. If I understand correctly, in Haskell this can be done with the Either monad and liftM, though I can't claim to understand the specifics.
Is there a Rust solution?
Yes, the default semantics is that it will stop when the first comparison fails and give you that error. You can write slightly different codebase if you want a list of all errors instead.
- It's hard to spot outliers. This leads to occasional bugs that tend to get easily overlooked in code review. Also, it makes code reading harder when an "if err" block is subtly different. The most common case here being "if err == nil" (sometimes bug, sometimes on purpose) - super hard to notice.
- You say you have seen missed "if err" blocks only a few times. I say that's 100% too many; every one of them in my experience was a subtle bug (possibly comparable to off-by-one errors in C).
- When I need to focus on understanding/analyzing the optimistic path in a fragment of code (always the first thing I do when reading), the everpresent "if err" blocks introduce tiresome visual noise and make the reading/grokking process slower and harder (having to constantly try and mentally filter out some 80% of what my eyes see).
Also, what editor/IDE do you use? The reason I ask is because of this: https://youtrack.jetbrains.com/issue/GO-7747
if err != nil { return ...,...,err }
If there were a shortcut for returning that error + zero values without interfering with the function call which produces the error (as try does), I'd prefer it. Something more like check(err). We'll see what they come up with next though to try to address this.
I can't say I've ever had problems tracking down an error, not sure what you mean about default values - surely if you check the error you won't use the values returned. My only problem with go error handling is the verbosity, which isn't a huge deal.
Same could be said about assembly language.
My impression of go, is that it is very boring to program in it, and some decisions weren't thought well. For example if you use anything else for numbers than int, int64 or float64 you will have very bad time. Lack of generics forces you to duplicate your code, duplicating increases chances of errors and make it harder to fix bugs. The errors are passed as values, but then you need to use different return value to pass them, defeating the whole point of having that. On top of that the language is very rigid.
I'm wondering if introducing macros could solve a lot of those issues.
Could you give an example?
I think you’re talking about something different than what I’m understanding. One of the major frustrations I have with Go error handling is the lack of stack traces, which means I often have to modify code in order to find out where an error occurred.
I’m pretty sure that’s not what you’re talking about, though.
Don't you just mean you knew where fatal exceptions could be raised? That's substantially different from "fail".
That's also why I think Go will forever be a "to each his own" language. By targeting that lowest common denominator, choices are made for you, some people will always dislike it, and others will always love it.
For example, the semantics of the for-over-collection statement in Java are that it repeats the block of code, with the loop variable holding each value of the collection in turn. In Go, the equivalent statement also needs to document what it means to take the reference to the loop variable. Composite data types have semantics for taking references to sub-parts (struct and arrays/slices allow taking references, maps do not).
Slices and arrays are another complicated area of the language, with gotchas like append sometimes modifying the original array, sometimes not.
Regarding splits, you still have that in Go as well - do you use goroutines as coroutines, sending copies of objects through channels? Or do you use them as threads, with shared memory and locking? Do you use the testing package as is, with its lack of any user-friendly asserts? Or do you pick up an assertion library? Do you use raw http,or something la Gorilla? Sql or some ORM? Do you log to stdout, or do you pick up a logging library?
Regarding gofmt, I personally can't understand the passion some people have for enforcing a common style.
Regarding build tools, Go is hardly unique among modern languages in having built-in tooling for that. It's relative ease of deployment also means that Go's build tools can't be used alone in a multi-language project, so many of the apparent simplicity is only useful in a subset of projects that actually use only Go. Even then, if you have any other build artifacts you may find you need to reach for something other than Go's tooling. By comparison, Maven can easily handle a Java+minor bits-in-other-languages project out-of-the-box.
Not to mention that Go is probably the only language in any kind of popular use today that doesn't have a built-in way to interface with C code (you need to use a separate compiler if you want that!).
To me, Go has a single killer feature: binary and memory size for a GC language. And Java may be catching up on that area...
> Go the language is surprisingly complex and error prone compared to other GC languages because of its decision to allow explicit references.
It's true that having value types in addition to reference types (while other GC languages often only have reference types) adds some complexity to the language, it's not much and it's still much less complex and error prone than other GC languages. Also, C# has value types and Java desperately wants them, so I think it's pretty clear that they're worth the extra bit of complexity.
> This adds an extra layer of semantics to nearly every aspect of the language.
I don't think this is meaningfully true. You have to think about whether a thing is a value or a reference type. This is exactly one bit of additional complexity.
> Slices and arrays are another complicated area of the language, with gotchas like append sometimes modifying the original array, sometimes not.
Slices are actually quite simple, but people run into issues because they expect them to behave exactly like Python lists or JavaScript arrays. Slices are views into an underlying array, and appending to a slice always modifies the backing array; however, if the append causes a grow, then the backing array is now a different array than the original. Of course, your point stands in that this difference can be frustrating and that frustration is a real cost--but you run into that cost once or twice and you update your understanding and rarely encounter it again.
> Regarding splits, you still have that in Go as well - do you use goroutines as coroutines, sending copies of objects through channels? Or do you use them as threads, with shared memory and locking? Do you use the testing package as is, with its lack of any user-friendly asserts? Or do you pick up an assertion library? Do you use raw http,or something la Gorilla? Sql or some ORM? Do you log to stdout, or do you pick up a logging library?
I never claimed Go makes every decision for you, only that there are fewer decisions and the happy path is more obvious. You always use goroutines as threads and whether you use channels or locks is a design question (different use cases). The standard testing library is the happy path. You can add on an assert library if you really need it (although you probably don't). Similarly you use the raw HTTP library until you really need something more (you probably don't). Stdlib database/sql package or ORM? Again, database/sql until you need an ORM (again, you probably don't). Standard library logging package or a logging library? Again, you use the stdlib (happy path) until your requirements outgrow it. Notice the pattern?
> Regarding gofmt, I personally can't understand the passion some people have for enforcing a common style.
It avoids wasting time in nitpicky style conversations in code review and makes things easier to read. You're welcome to your opinion, but that's the rationale.
> Regarding build tools, Go is hardly unique among modern languages in having built-in tooling for that.
I didn't claim otherwise, only that Go's standard build tool is yet another example of simplicity and consistency. And many languages don't have a standard built-in tool, and the ones that do are often complex. The only language with a nicer build tool IMO is Rust, and like everything Rust vs Go, the Rust build tool prefers pragmatic complexity to simplicity (a philosophical difference that I can respect).
> Even then, if you have any other build artifacts you may find you need to reach for something other than Go's tooling. By comparison, Maven can easily handle a Java+minor bits-in-other-languages project out-of-the-box.
I don't see why every language should have a build tool that can build it + small bits of other languages. A build tool should build that language well, and extend it for bigger projects using a wrapper tool like Make or Bazel depending on use case. Unix philosophy and all that.
> Not to mention that Go is probably the only language in any kind of popular use today that doesn't have a built-in way to interface with C code (you need to use a separate compiler if you want that!).
Every language (even C++) needs another compiler to compile C code before it can be called into. And once it's compiled, it's no more difficult to call into it from Go than from Java or C# or Python or etc. It's also often easier, since you can use C values directly in Go without needing to write shims (e.g., PyObject shims). That said, like all GC languages, it's fundamentally hard to correctly manage ownership for references across the C/$LANG boundary.
My point was not about value types, but about taking the address of some thing; neither C# (except the extremely rarely used `unsafe` subset) nor planned versions of Java have this ability. To be fair though, C# does have something somewhat equivalent - lambdas may capture a local variable such as a loop variable, in which case you do need to know if each iteration creates a new variable or changes the value of the same variable. Java doesn't allow this at all. Even in C#, you can't capture a part of a structure, so the complexity is more limited.
There are other aspects to Go's complexity as a language - multiple syntaxes for declaring a variable, multiple other ways of declaring constants, iota, named return variables, function scope VS local scope, the limits of what can constitute a map key, what kind of structs can be compared for equality,special syntax that looks like multiple assignment but isn't, and probably others. My point is that Go is not a very simple or consistent language. Java for example is still simpler. C# is more complex, but more consistent.
> I don't see why every language should have a build tool that can build it + small bits of other languages. A build tool should build that language well, and extend it for bigger projects using a wrapper tool like Make or Bazel depending on use case. Unix philosophy and all that.
The reason is tracking dependencies. If I have a pure Go project, I can rely entirely on modules to manage my dependencies. If I need one python package, I now need to find a new tool to declare my dependency and orchestrate things. With Maven, for example, I can just export the python package as a Maven module and keep using Maven for my entire build.
To me, a dependency management system that is strictly language specific is only useful as a starter tool - almost certainly, once you are working on a real project, you will drop that tool entirely and have to use soemthing else, as dependencies are a cross-language problem.
That said, it is nice to have a dependency tool available as you're getting your feet wet, so I shouldn't really be complaining that Go offers this.
> Every language (even C++) needs another compiler to compile C code before it can be called into. And once it's compiled, it's no more difficult to call into it from Go than from Java or C# or Python or etc.
First of all, I must admit that I was wrong about how Go handles CFFI. I was under the wrong impression that cgo is an alternative to the standard go compiler, and that you have to build your program with cgo instead of go build in order to be able to link to C code. Since I now understand that cgo is simply a tool to generate the necessary boilerplate to interoperate C and Go, you're right, it's actually much nicer than what Java or Pyhton offer. Note, I am aware that you need a C compiler to build your C code; I just thought that there are 2 Go compilers, one for pure Go, and a different one for Go plus dynamic linking to C.
C# is still simpler, since it doesn't need any external tool or C compiler - you simply declare the C# headers for the C functions you want to invoke, and annotate them to declare the shared library they should be searched in, and the CLR handles the glue for you.
---
There were a few other points, like gofmt where we simply have different experiences and I don't think it's productive to argue. There are others where I had misunderstood you as claiming Go is especially good at, and I understand your point was simply that it checks those boxes, which I agree with.
I'm curious about what you think these other options are? There are of course tons of languages and you can find just about any set of features you want, but the combination Go has along with the momentum, community, etc. it has means that there are not many that can compare in its niche.
Ignoring the language/semantics (ie. the bad ergonomics) and just focusing on the external aspects. Which languages targets the same niche that have the "similar benefits" of fast compile times, static builds, great tooling, good/improving libraries, easy deployment, and mainstream backing/acceptance?
Which is? "devops"? Otherwise, offerings on the JVM and .NET are strictly superior. And now with native compilation being available for both platforms, they will also be a better fit at devops than golang. Other alternatives include Rust.
JVM based language can't compare due to the JVM. The last thing you want for that type of programming is a huge, complex virtual machine to worry about. That plus memory issues, slow compiles, complex deployments makes it not really in the running.
The .net based languages are a bit more interesting, but they have a _lot_ of ground to make up for due to their roots. Not having their roots in the native OS/culture of the internet will make it tough road.
Rust is nice but has its flaws as well. I think it will end up nailing a lot of use cases where C++ would have been picked before. Go really targets what JVM based languages or things like Python+C would have been used for... not C++. That is, I think it is really a different niche.
You should probably tell Google and Amazon that. Both very heavy users of Java on the server, despite Go. In fact the server space is where Java is most dominant!
The JVM is not actually complicated to use, especially not in the latest versions. It has the same number of 'default knobs' (that you should probably tweak in production) as Go does, but they're simpler to understand, for instance, Go requires you to control GC overhead by specifying "heap overhead" whereas Java just lets you specify a pause time goal directly, which is what you actually care about.
As for slow compiles, where did you get that idea? Java is extremely fast to compile. You can compile hundreds of thousands of lines of code and start executing them in seconds. I wonder to what extent you really used the ecosystem at all, with comments like that.
Yes, for the smaller companies that do not have the effectively unlimited resources of Google and Amazon. Go is just so much easier to work with and deploy for my tiny company.
Sorry, but I don't buy this at all. golang also ships with a runtime, that's why binaries are dozens of MBs.
The JVM is very configurable, which is what I assume you mean when you say "complex". This configurability is what allows it to be tuned to the task at hand. You can cap the maximum memory it's allowed to use, try out different GCs based on your use cases, etc.
Compilation times are not slow the least bit, especially with incremental compilation. Incidentally, for the majority of the time, it takes way less time from when I press the run button for a Java/Kotlin project in the IDE to having it up and running, compared to projects I worked on in golang, where the entire code base has to be rebuilt.
People keep repeating things like "memory issues" or "complex deployments". For a long time now, people have been shipping self-contained "fat" or "uber" jars. Running them is as simple as `java -jar program.jar`. And since the JVM defaults to G1, memory usage is generally less - the JVM is tuned for performance at the expense of memory saving, but that can obviously be changed, even more so recently with GraalVM[1]. Java has already been used on embedded systems (including card readers) - even Spring that has a reputation for being "bloated" can run on a Raspberry Pi.
That is several steps too complicated:
1. Do you have Java installed? 2. Is it in the path? 3. Do you have the Required Version of Java installed?
None of those need be issues with a self-contained compiled Go program. And that is one of the key reasons we chose Go over any other language.
(And for the other languages that can do the same, Go was the easiest to learn and potentially master.)
Kotlin is now the official language of Android, and is being very rapidly picked up all across the Java ecosystem. Oh but it also compiles to JavaScript and LLVM native binaries if you don't want the JVM.
The syntax and ergonomics of Kotlin are great. Really nice language. The compiler is also very fast, just as fast as Go's I believe. It does incremental compilation and all the mod cons.
Enjoy your ecosystem, I'm going to continue to use languages that actually like me, and care how my experience is.
I'm not sure I follow your argument about Go "approaching" checked-exceptions in Java. Nothing is being forced on you. Go has worked the same way for a decade now. There is a lot of hugely successful software written in Go. In all that time, errors have worked the same way. In terms of choosing principles over ergonomics, in fact the Go committee chose ergonomics over the mistaken principle that "explicit error checking is so ugly and onerous that it's worth adding magic behavior and removing clarity".
In any case, no one is arguing we shouldn't or couldn't improve Go's ergonomics. Rather, the conclusion from people who work with Go every day and are happy with the tradeoffs for their particular projects was that this specific change would make more things worse than it made better.
It might be fair to say the ergonomics are suboptimal for the code _writer_, but I would say they are much closer to optimal for the code _reader_.
All of this is also true of the generic implementation with Result. Except that (IMO) it's much nicer to work with.
Is it fair to compare external packages to core language features?
There is nothing preventing a go mod/dep left-pad
You need a value that is either correct or an error, not something that returns two values.
Very refreshing for novice to agree on expert on a topic.
If so, where’s (s)he buried? :-)
Maybe something like vectored error handling (like in Basic) would fit the language better - was curious why Golang didnt implement that - seems a natural pattern their model would work well with.
Along these lines, I would have much preferred to see the opposite of this proposal: the total removal of named returns. They add magic and confusion, and they are redundant with normal returns. Except in the absolute simplest and shortest functions, I always reject named returns during code review. They are currently the only example of C++ style policy declaring we-don’t-use-that-feature-in-our-org I’ve run into with Go. Try/check would’ve been the second.
> that's less reliant on dependencies. "A little copying is better than a little
> dependency" and you can see it in stark contrast to something like the JS community
> with its require('left-pad') NPM ecosystem.
The dependency-hell of node projects is in no way whatsoever related to sensible, accepted programming paradigms like generics. Their absence from Go is a widely accepted shortcoming of the language. It doesn't make Go a bad language, and it doesn't reflect on you or anyone in the Go community who enjoy using the language, but you don't have to try so hard to rationalise their absence, especially since they're one of the more prominent things in the Go 2 drafts.
"A little copying", "less reliant on dependencies"... do you even vendor bro
https://go.googlesource.com/proposal/+/master/design/go2draf...
Seems either off-base or poorly worded.
You can emulate this with a boolean flag in your RAII types in Rust or C++, that's set or cleared immediately before a successful return, and then doing conditional logic in your Drop/dtor. Or you could do a std::mem::forget before successful returns. But I guess they think this is an important enough case to dedicate syntax to it for ergonomic reasons, which Rust doesn't have - which is what I think they're getting at.
You can use the match construct for that, and the Result<T,E> type comes with some utility methods that make it easier to clarify your desired semantics in many cases. The page complains that the "match" syntax is clunky, but I'm not that sure how 'handle' is supposed to be better.
`Result` combinators like `and_then` only work when the error types are the same.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Or, if we want to keep a more 1:1 direct mapping to the Go code:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Caveats with the "1:1" mapping:
1) In a real rust codebase you'd probably simply forward std::io::Error instead of converting it into a string like I have here, or give it a better error struct/enum type. I've tried to mimic the Go code here, not fully convert to Rust idioms.
2) You could get rid of the .as_ref() spam by just using &str or &Path to be closer to the Go code, but I'd rather stick at least that close to std::fs::copy's file signature.
3) All explicit close operations are dropped as unnecessary vs the Go code. I guess I could've used std::mem::drop to be more explicit?
4) TempPath is obvious overkill for the single remaining error point
5) In temp-file heavy code you'd probably wrap TempPath + File into TempFile. keep could return the interior File as well.
In all the real code bases I've worked on, there are multiple disparate types of errors that nevertheless have the same context.
Example: A function that takes in a path and parses a config file at that path fails if it can't open the file or if the file is malformed. The file can be malformed because indentation is wrong, because there's a string where there should be an integer, or because a required field is missing. All of these are different error types.
So a single `std::io::Error` is not possible, and erasing them into a `Box<dyn Error>` or wrapping them in a custom (context-containing) type nevertheless requires writing a `.map_err` per each Result value.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
EDIT: That said, adding extra context will often require map_err or similar. But merely type erasing / combining error sources shouldn't need it, unless I'm missing something. (Most of my Rust use so far has been on toy codebases...)
1. `From` is a global solution to a local problem. All `std::io::Error` must necessarily be converted to the same enum variant regardless of what caused them. Failing to open a file and failing to write to a network socket will create the same variant.
2. `From` does not have access to context anyway. A `From<std::io::Error>` is not going to know that the error was hit specifically when "parsing the /etc/foo.conf file", and a `From<std::string::ParseError>` is not going to know the error was hit when "parsing the bar field of the /etc/foo.conf file because it is set to `baz` which is not an integer".
>as long as your error types implement std::error::Error, you shouldn't need an explicit .map_err to box them:
This only works when you want your function to return `Box<dyn Error>` itself without any additional context. The conversation was about needing to add context like in the golang example.
I think Go sometimes swings too far in the simplicity direction, not letting consenting folks choose to use shorthand, but that’s a very valid design decision.
For example, the example returns a custom wrapper around the original error with context that it was a copy operation with such-and-such source and destination. The equivalent in Rust using `failure::Fail::with_context` requires writing the `.with_context(|_| format!("copy: ..."))` on every expression that uses `?` (unless you happen to get lucky and all the inner errors are the same type, so that you can use combinators to combine them into a single `?`-able Result).
Edit: And to be clear, this is not limited to `failure::Fail`. Using your own `enum Error { Copy { source: PathBuf, destination: PathBuf, inner: Box<dyn Error> }, ... }` still requires you to write a manual `.map_err(|err| Error::Copy { ... })` after every Result value that you intend to use `?` on.
Unfortunately, the community that has sprung up around Go is more or less opposed to new language features on principle.
I think this comes straight from the original go team. Rob Pike had a talk[1] that is partly about why go doesn't keep adding features and why it doesn't have certain features that other languages have. I think people who like go have bought into the idea that the go team has made good trade-offs to make go code easier to read and maintain at the expense of expressibility.
There's a lot of value to there being "one way to do things", even when it's not the best way from any particular point of view.
Go holds this principle higher than most other languages and I think that should be either embraced, or one should look elsewhere - and I'm saying that as someone who "looked elsewhere".
I suspect some people are. Languages have network effects, which get stronger when FFI is uncommon. My team and codebase mostly keep me on Java even though I find Kotlin clearer in every way and I'd love to get better with Scala and Haskell. If for some reason my team switched to something like Go or VB I'd have to find another job or else put up with it.
Using a programming language is not always an option.
I really like how errors are handled in Go and I hated this proposal.
The current implementation forces you to constantly think about errors at each single point and it is extremely good at standing out. This is something very valuable, not something that requires or needs to be hidden behind syntactic sugar. It improves the readability of the code and the quality of the software.
If it ain't broke, don't fix it.
Here's the thing, some of us don't want Rust. It looks great! It's perfectly awesome for it's primary domain (i.e. re-implementing critical portions of a modern browser). But it's not what I want to use as my daily driver.
I'm writing stupid shit that's munging CSV files and transforming json documents. I want a language that mostly gets out of my way, but performs well and can scale beyond a couple thousand lines of code. Go fits that space nicely, even with its (or maybe because of) conceptually imperfect error handling.
I get that certain aspects of Rust make that easier (and certainly safer) but I'm not working in a domain where the performance gains of ditching GC matter.
Of course, you can manually manage memory in Rust, but it is almost never necessary for high level applications.
I’m not at all claiming it’s the best tool for every job (although I do think it’s great), just clarifying the point about memory management.
I also spent a significant amount of time discussing with many people, including the Go team, and I am glad the proposal was declined, not because I like “if err != nil {…}” but because the proposal to add “try()” was not solving a good problem. Many, and I would say, every Go programmer wants better error handling, but “try()” was not it.
I hope the Go team keeps exploring other ideas to hopefully one day have a better error handler.
Usually to support this the language needs Enum support and a proper type system neither of which golang has. So I'm ok with the developers just baking in syntax for an error monad with specific sugar for extracting the value or handling an error.
Special purpose syntax can buy you a lot more. For example Swift’s try makes it obvious which statements contain error handling without burdening each expression.
First: The monad allows for composition of functions. Returning two values does not. It breaks the flow of a function pipeline and forces you to handle every error in the same way.
Second: Extracting the value via pattern matching guarantees that the error will either be handled or used correctly. This is a way to 100% guarantee that there are No runtime errors. That's right. Using the error monad with pattern matching makes it so that there is zero room for runtime errors.
Why create a language that has runtime errors? Create a language that forces you to handle all possible runtime errors before it even compiles.
According the criteria of safety, zero runtime errors, and expressivity via composition the monad is the Best solution. There is literally no other way of error handling that I know of that can catch runtime errors at compile time.
I know you tried to flip my statement on it's head by using the term "criteria" as if there are many many different criteria for "best." And you are right, programming is an opinionated thing. However, ZERO runtime errors is a too powerful of a feature to assign to a specific criteria. Such a feature is so powerful, it should be part of EVERY criteria.
If you don't understand completely what I mean by "composition" or how pattern matching and a maybe monad can guarantee a runtime error will NEVER occur, ask me to elucidate, I'm happy to clarify.
Presuming you know how the maybe monad (or similar named monads) handles errors. A MaybeOutOfMemoryError works in a similar way in the sense that any attempt to use the value meaningfully will force you to handle the error.
data MaybeOutOfMemoryError a = OutOfMemoryError | Some a
ioFunction :: MaybeOutOfMemoryError a -> IO
ioFunction OutOfMemoryError = println "ERROR"
ioFunction Some _ = println "No ERROR"
exhaustive pattern matching with the error flag enabled makes it so that if you forget the OutOfMemoryError case an error will occur during compile time.Any function that can potentially trigger a memory error should be forced to be typed like this. However in programming, ALL functions can potentially do this, and side effects outside of the scope of the function (total memory available) will trigger it meaning that the only way to do this effectively is to make ALL functions typed this way. Which is inconvenient to say the least.
That being said you can just type the main function as something that can only accept this monadic value forcing users to wrap this handling in a top level function:
myWrittenProgram :: MaybeOutOfMemoryError a
-- not defined in this example
-- main is forced to be typed this way just like how in haskell it is forced to be typed IO ()
main :: MaybeOutOfMemoryError a -> IO ()
main Some a = printLn a
main OutOfMemoryError = printLn "Error out of memory"
Obviously the above has some issues with the rest of haskell syntax and the way things are done, but I'm just showing you a way where it is possible to make sure that a memory runtime error does not happen (that is an unhandled runtime error).That being said unless you know haskell or know what a maybe monad is, you wouldn't be able to fully comprehend what I said. Exception monads exist in haskell, but nothing is forcing the main function to be typed this way.
That is, the verbosity would certainly benefit from some sugar, but the semantics -- errors managed by separate expressions/blocks immediately adjacent to the error-generating code -- is fundamental to the language, and one of its great strengths.
So yeah, it's actually quite fast to dig in a Golang codebase. You notice that as a normal user when you find it faster to read the standard library vs reading the doc, or when you read an implementation instead of reading a spec/algorithm to learn about it.
My guess is that gofmt is a huge factor in this, but also the fact that there's not a huge amount of built ins, anf that the standard library is pretty complete.
It really is. My code looks like your code and the next person's code. Go is opinionated and strict and that makes reading other people's code so much easier
IMHO we're about 10 years overdue for committing a canonical representation of that AST to version control instead of treating a particular serialized visualization of it as the single source of truth (and spending countless hours debating which format is "good enough" for everyone in every situation).
"The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt." – Rob Pike
First, the docs link to the actual line in source, so it’s just a click away and available in the exact context I need it.
Second, I know I’ll be able to understand it: there are usually very few dependencies, so I can usually get all the context I need from a single file; the source formatting is familiar; code style, like variable names, are familiar because of the cultural influence of the Tour of Go; and there is usually no magic anywhere—I know that things are exactly what they appear to be, like when I see a variable declaration, it is not secretly calling a complex function (this is vital to human knowledge and is aligned with Objectivist epistemology’s “law if identity”).
Third, every time I look at the standard library source, I become a better programmer. I’ve lost track of the number of times I’ve thought to myself “oh, that’s a clean way to organize this kind of code!” I often end up immediately using what I learn from the stand library source.
One of Go's major selling points (for me anyway) was a solid standard library written mostly in conventional Go style. It's a great example of how Go code should be written, and a wonderful learning tool for the language.
It also makes participating in the community easier because that's one less thing you have to learn in order to contribute.
a = append(a[:i], a[i+1:]...)
That’s the recommended implementation of erase(). After this, is the original object referred to by ‘a’ modified? How can you tell?Let’s pop from a stack:
x, a = a[len(a)-1], a[:len(a)-1]
Did you read that 100x faster than ‘a.pop()’?Now this:
a = append(a[:i], append(make([]T, j), a[i:]...)...)
This is an operation called “expand.” What does it do? It is an honest question, I have no idea.These are completely idiomatic examples taken from the Go wiki. They are not readable.
I think this is one of those mistakes in the language that exist because early-on Go could have been a true systems language but it's been adopted by a large number of devs more as an infra and high-level automation language where correctness is more important than performance.
Don't get me wrong, this isn't a general-purpose argument and I'm not one of those people who thinks that the language should completely get out of your way: eg I'm not a Rust user but its unergonomic handling of memory safety seems far preferable to the ease with which you can shoot yourself in the foot with C++. I definitely see the appeal of Go's handholding in a directional sense. But the degree to which it takes it makes it feel like an unserious or educational language, unnecessarily difficult to get actual work done in, like Javascript but for the exact opposite reasons (and to be clear, Javascript is INFINITELY worse).
This makes it sound like Im more negative on Go than I am, but I think it's the first time I've been able to articulate what deflated my initial interest in it and kept me away from it. Perhaps if feel differently if I had still been a student and new to programming when Golang was released.
And there's nothing wrong with that, that's exactly Go's target audience.
I've used Go professionally for 6 years and love it, so the patterns are ingrained in my head and don't bother me. But it seems pretty clear that it's much more arcane than alternatives.
Tell me again how easy it is to debug Go. In other languages the compiler can just tell me in _seconds_ that I done fucked up.
If you response includes "You're doing it wrong if you have deeply nested framework code" then you can rightly fuck right off too.
> If you response includes "You're doing it wrong if you have deeply nested framework code" then you can rightly fuck right off too.
I mean, can you point me to an example? Absent more context, I'm pretty confident that you're indeed doing at least something wrong...
That's a pretty common use case but ignore that for a moment and just distill the problem down to it is possible to have null pointer issues in Go code.
Another example eith sufficient use of goroutines and channels it gets really tricky to debug something when a receiving channel blocks because something isn't sending.
There's plenty more. I don't find Go any simpler to debug than Java, Kotlin, Rust, or Python and in many cases it is significantly more difficult because you're constantly fighting the type system or working through huge amounts of boilerplate.
I often get the feeling that Go users are implicitly comparing it to languages like Python or JavaScript.
I've watched a client try to debug a Go codebase they had. It was sad. Their web server just returned 500 Internal Error and they tried to track down why from logs, but by the time the error had made it back up to the serving loop most information about where it came from had been lost. I suggested they attach a debugger to a server to see what's happening, they said debuggers don't work that great in Go and so people don't use them much.
If they'd been using exceptions they'd have a stack trace and could have pinpointed the source of the fault in seconds.
I hope they take another stab at improving error handling. I like Go a lot and do think error handling is one place it could use improvements.
It would also appear to be amenable to an automated tool that would convert to or from try-style.
Given that, why would people have strong feelings against? Because they think go needs something better, and if this passes, they'll never get it?
I agree with this. I think many Go users like Go because of its simplicity that prevents unnecessary bikeshedding so prevalent in other languages.
I don't have a strong opinion on try, but this is not a good philosophy. Whatever you include in a language will get used and will get abused if it can be abused.
This is mostly why there is a part of Golang users who do not want generics. Look at codebases in C++ and you can see the clarity cost of adding generics in a codebase.
More that they will never get rid of it, even after something better comes along. Same reason Go has been careful about adding generics. There have been many generics proposals over the years. Any one of them – or even all of them – could have been implemented, but all of them had faults that the Go maintainers did not way to forever saddle the language with even when a better way to do generics comes along.
Worse yet, Go community may develop, or already has, a taste to the lack of genetics. When a really really good proposal comes along, and is implemented, it may produce a Python 2/3 rift with the existing code bases, or not be accepted at all because of this.
IMO, this is because with generics (and we even get shades of this in error handling), we are running into limits with the type system itself. Coming up with a proposal that’s universally acceptable for generics would probably require a bottom-up (and potentially compatibility-breaking) refactor of the type system. Would that be this worth the effort? Eventually, probably, yes, because you might unlock other benefits for free (compilation time, GC efficiency, etc.).
Today, however, there are many lower hanging & valuable fruit to pluck. In the meantime, new languages with better type systems are invited to capture market share. :)
It complicates the language for little gain. It's good that they punted on this for now.
https://pbs.twimg.com/media/D_ojiEqUYAAhmrH.png
edit: s/%s/%d/ but you get it.
I was pissed at first. However, now I cannot code in any language without overusing try and being scared of each line.
Using Go's error handling is actually making your code smarter, I mean, you don't want your code to break with a weird message because of something stupid.
The simplest example is adding a default-path whenever I'm reading a file, Go's error handling reminds me of what to do if this file doesn't exist! Or cannot read etc.
Don't take my word for it, Go ;) try it.
with {:ok, val1} <- happy_path1(val0),
{:ok, val2} <- happy_path2(val0, val1),
{:ok, val3} <- happy_path3(some_val) do
function_might_crash_let_it_crash!(some_val)
happy_result()
else
{:error, :enoent} -> handle_notfound_error()
{:error, :eperm} -> report_permission_error()
_ -> raise("don't worry this process is supervised, let it crash!")
end
low cyclomatic complexity makes for a nice user experience, and you learn the philosophy of "if it doesn't work, just turn it off and on again". Why be scared? Just let it go. The VM has your back.Rust's `match` also does an ok job
func do(a Param) (res Result, err error) {
var aPrime T
if aPrime, err = getResource(a); err != nil && err == RecoverableError {
aPrime = definitelyNoErrorAlternativeGetResource(a)
}
var aPrimePrime T
... // many more steps
return doSomething(aPrimePrimePrime)
} @spec my_fn(a::input_type)::{:ok, res::result_type} | {:error, any}
def my_fn(a) do
a
|> get_resource
|> case do
{:ok, result} -> result
{:error, :recoverable} -> get_alt_resource(a)
err = {:error, :reallybad} -> throw err
end
|> many_more_steps_might_have_similar_pattern
catch err
err
end
However what you presented seems to me to be an antipattern. Sometimes code clarity trumps terseness; in this case the results from get_resource and get_alt_resource are categorically identical. If we are allowed to refactor to be actually sane code and not require everything to be in a single function: def my_fn(a) do
with {:ok, res1} <- resource_wrapper(a),
{:ok, res2} <- next_step(res1), ...
case
#handle errors here.
end
end
@doc """
fetches resources of type `your-favorite-type` which
can either come from `get_resource` or `get_alt_resource`
"""
def resource_wrapper(a) do
case get_resource(a) do
ok = {:ok, a} -> ok
{:error, :recoverable} ->
get_alt_resource(a)
# note with this code if alt_resource
# gives you an error tuple you are still
# good to go
err = {:error, :fatal} -> err
end
end
seems much more saneyou can manage it a few ways in Elixir, struct matching is one:
def perform(param) do
res =
case get_resource(param) do
{:ok, val} -> val
{:error, %RecoverableError{alternative_value: val}} -> val
end
do_more_things(res)
end Result do(Param a) throws Exception {
T aPrime;
try { aPrime = getResource(a); } catch (RecoverableError e) {
aPrime = definitelyNoErrorAlternativeGetResource(a);
}
T aPrimePrime;
... // many more steps
return doSomething(aPrimePrimePrime);
} try {
var val1 = happy_path1(val0);
var val2 = happy_path2(val0, val1);
var val3 = happy_path3(some_val);
function_might_crash_let_it_crash!(some_val);
return happy_result();
} catch (NotFoundError e) {
handle_notfound_error();
} catch (PermissionError e) {
report_permission_error();
} catch (Exception e) {
throw new Exception("don't worry this process is supervised, let it crash!");
}IIRC, the VM exits, so, it's not at all identical. There are potentially severe nonlocal effects and you're already coding defensively by putting a catch/rethrow.
The re-throw was just to mimic your Elixir example. Normally I'd just let exceptions that shouldn't be handled at that level bubble up the call tree to wherever it makes more sense for it to be handled.
Now, I'll admit, most Java code I've seen professionally use exceptions badly, in that they tend to be overly aggressive and defensive, try-catching everywhere at all levels when they should just let things bubble up as appropriate. I believe that's just a general misunderstanding though. Often it is caused by Java's use of CheckedExceptions, programmers just try/catch to make the compiler error go away, when they should just re-declare the exception since it doesn't need handling most of the time, or re-throw a runtime error if they don't care to have compile time checks for handling them.
When I code in Java, I barely ever try/catch anything. Normally I have one try/catch around my main method which can recover the whole app from any error, and from there, if there are smaller process trees that can be recovered independently at their level I sprinkle a few more try/catch in those places.
P.S.: I also don't want to get into too many details, but the VM actually doesn't crash, Java will call your UncaughtExceptionHandler for any given thread that crashes, the default one performs a System/exit in error state, but for a server app for example, you'd handle it.
And now years later I couldn't imagine going back to using exceptions. The non-linearity of exceptions creates such a cognitive load of picking up unfamiliar code, and this is where go really shines. I can dive into almost any go codebase and get a lay of the land very quickly.
On another note, I've been enjoying rust's approach with Result<T,E> / the ? operator. Same principle with less visual bloat.
For the record, the `try` proposal which was just declined, is almost equivalent to the `try!` macro in Rust, which was the "testbed" for the `?` operator.
Personally I find go error handling okay but that’s because I exclusively use panic().
I've published plenty of Go code with a panic() here or there to handle fatal issues, and there's never a shortage of ignorant fools dropping in to lecture me.
If I'm not supposed to use it, why is it even in there? To tempt me? That would fit with the glorification of pain and boredom, I guess.
Curious, are there full-time (or at least Primary) Go developers that are upset by the lack of Try?
I use golang at an employer. error handling in golang is verbose, error prone, distracting, and difficult to make sense of when there is actually an error (composing). I've seen on several occasions now errors being mishandled (either dropped by accident, or by overwriting already existing error variables in the same scope). These issues don't happen with exceptions.
About overwriting, again this has never been a problem for me (as far as I can recall). Error values are very much localised in almost all Go code I have seen and written. If a function returns an error it is either immediately acted upon or returned by the caller. As such, any overwriting done at a later stage is more or less for convenience sake.
Instead of designing a language that prevents errors from being dropped by accident, hire a bunch of literal gophers to search your codebase for inverbosity. Good plan will work 10/10 times.
I am a full-time Go developer. Error handling in Go is miserable. `try` would have made it slightly less miserable.
GoLand gets around this somewhat by adding in the Live Template of "err" being a macro expansion for the if err != nil { return }. However, it still adds those 3 lines below each requisite call.
If they added a new keyword that took the place of this macro, would that be too intrusive? Clearly, this only works if you have named return values.
It would be nice if there was a bash/Ruby-like chaining, such as:
var1, var2, var3, err := call1(args) &&
var4, var5, var6, err := call2(args) &&
... and more calls and so on ...
return callN(args)
Where "&&" would perform the if err != nil { return } in-line. Should the first call return a non-nil error as the last argument, flow would be returned to the caller. If not, the flow continues as normal.This could even be extended in the case of function chaining:
return call3( call2( call1(args) && ) && )
The downside of using && is that it's overloading the && and may cause some compiler/developer confusion. This was just an example based on a familiar use case (bash); another token can be used.Rasky suggested something like this based on another proposal in the thread, but using "?" instead.
Another way to approach this might be an overlay language analogous to Kotlin. There could be a Go dialect that compiled into Go that provided this feature. Or, possibly an optional "plugin" for go's compiler that added an intermediate transformation step prior to compilation based on new keywords, but this will frustrate debugging and lead to other surprises. Generators tried to do this, but it doesn't seem to have taken off, from the repos I've read.
Just my 2c
In Python or Java, you see a lot of try-catches around blocks of code, which can obfuscate where the source of the error is, despite having particular handling for the type of error.
For systems code, I mostly care about whether something failed... like: - Was the socket opened? - Was the file created? - etc.
Apparently (early) Java was designed in much the same way, just to a lesser degree.
I'm not a fan of languages specifically designed to limit me from expressing myself. Then again, I work on small/disciplined teams, where this tends to work out pretty well.
It's just that I personally do not want to work on a project for which the technical need to repeated uncreative execution. I want to explore new ways of solving problems, and Go is not the language for that. That's why use of Go is a reliable signal.
Not being creative about error handling (for example) frees some of the creativity budget for things that I personally find much more interesting.
Lack of abstractions (a.k.a "cleverness") on the language level, pushed developers to abstraction on the code level, often based on reflection and supported by XML files and (later) annotations.
In retrospect, by avoiding cleverness in Java, we ended up with multiple incompatible framework dialects which implement their own cleverness.
Go avoided this fate so far by having weaker support for reflection, and since the collective developer memory of Java framework excesses is all too fresh. This makes Go code more readable than Java on average, but I don't think it's any more readable than idiomatic code in a high-abstraction language like Rust, Kotlin or Swift - once you learn and internalize how these languages work.
Java's modeling and abstraction capabilities way supersede what golang has to offer. This is much better now especially after Java 8 and continues to evolve.
Citation needed.
> we ended up with multiple incompatible framework dialects which implement their own cleverness.
The trend for a while now is using libraries on a per-need basis. E.g. [1]. This also doesn't sit with reality based on the different JEPs available, and standardizations like JPA.
> but I don't think it's any more readable than idiomatic code in a high-abstraction language like Rust, Kotlin or Swift
I haven't used Swift of Rust in a significant manner, but golang is way less readable than Kotlin, and modern Java (8+).
I like my tools and projects I work on follow the exact opposite philosophy but I do see your point. It can be a lot of fun when the tools allow you to be creative. It's just that when working on real projects, I'd hate to be the person trying to understand someone's clever solutions. I dread it actually but may be that's just me.
I'm "community" and I would prefer to have `try()` than not to have. They didn't listen to me.
It is impossible to implement every proposal their ears listened to so obviously that's not what listened means.
I feel some in the go community have become convinced that the “go way” is to write the same code over and over again.
Implementing try now is splitting the community and introducing what will likely be new technical debt. It doesn't fundamentally change things, it only makes the language more complicated in a way that's both weird* and likely to become obsolete once generics are a thing.
They should focus on xerrors for now and wait until generics are implemented and try again.
* weird as in there are no macros and there's no other language construct that behaves like this so it's a totally unique and new thing for Go
Which I think is worse then try.
In a broader sense I am deeply irritated by the part of the go community's culture that seemingly sees any effort to make go more "don't repeat yourself" to be making it more complex. With no acknowledgment that avoiding complexity in the language isn't a free lunch, it comes at the cost of complexity in the code written, the ecosystem and libraries.
I created my own proposal that I think addresses the issues. I would love some constructive feedback. https://github.com/golang/go/issues/33161
It wouldn't have been the end of the world but I'm glad to see Go still has alive the spirit that made it so popular in the first place.
/cheers
What the try() proposal gets wrong is that it tries to automatically bounce the error back the stack, being equivalent to "if err != nil { return }", which is ofte reasonable, but at the same time lacks the flexibility that current Go code has in terms of augmenting the error, or indeed handling it: You'd end up with try() calls for some things, but not all, so it's just ironing out one wrinkle and not every wrinkle. The second argument to try() is too heavy-handed. What we need is easy, readable syntax for all cases.
In current Go code, people like the "v, err := someFunc()" syntax because it allows the happy path to stay in the current scope, with the secondary syntax "if v, err :=" introducing a new scope to avoid polluting the parent scope. If you're in the current scope, your code stays flat and clean (although Go's love of shadowing cause subtle).
In my opinion, we need a syntax that is similar to a "catch" block in other languages, but easier. Perhaps something like this:
v := getName() check err: {
return errors.Wrap(err, "could not get name")
}
This allows you to handle the error and augment it, while not introducing anything magical. It's exactly equivalent to an "if v, err :=", but without introducing a new scope and not polluting the current scope with an error variable. This syntax would happily support fallthrough: v := getName() check err: {
log.Printf("could not get name, ignoring: %s", err)
}
// v is now the zero value
And of course you could still support a syntax for introducing a new scope: if v := getName() {
// v is now valid
} check err: {
// You can return or anything else
}
You could easily extend it to error types with some kind of matching syntax: v := getName() check err == io.EOF {
// On EOF
} check err: {
// All other errors
}
The above syntaxes don't support chaining. I'm on the fence. I like Rust's "?" postfix syntax, and I think it could work: // This could be "monadic"; the first error short-circuits
ceo := getPerson()?.getCompany()?.getCEO() check err: {
return errors.Wrap(err, "couldn't get CEO")
}
But why not just let "." be smart about errors and fall through like this? ceo := getPerson().getCompany().getCEO() check err: {
return errors.Wrap(err, "couldn't get CEO")
}
After all, "." can know if the function returns multiple values, and "." on a tuple (or whatever Go calls it) isn't valid, so why not "promote" the "." to a high-level operator here?The awkwardness of the try proposal goes deeper than just error handling, too, I think. One reason Go can't solve its error handling problem elegantly is because of its reliance of multiple, exclusive return types. Almost all functions with this signature:
func getName() (string, error)
...obey the unwritten rule that the function returns either a valid value, or an error. If you get an error, the value is supposed to be unusable, and the way to check for it is to look at whether there was an error.†In many type systems, a situation where a return value can be either A or B is variously called a union, or discriminated union, or enum, or sum type, which is the term I prefer. It's always bugged me that Go's designers didn't go one step further and made sum types a first-class citizen, because I think it would fit Go rather nicely. TypeScript solves it this way:
function getName(): string | number
And you can also define types this way: type number = int | float64
This, incidentally, introduces the idea of optional values: function takesOptional(x: string | null)
Back to error handling, it would be more natural for a function to declare itself this way: func getName() string | error
After all, it returns a value or an error. It can never be the superposition of both.In this regime, anything that gets a value needs to explicitly check for what it is. "switch" on type would work, just like today, but that gets verbose for mere errors. So we just say that the "check" syntax as outlined above would work for any union that involves an error. In other words:
func getThing() Person | Company | error {
...
}
thing := getThing() check err: {
return errors.Wrap(err, "can't get thing")
}
We can do this because we can say that "error" is special and known to the compiler, just like make() etc. are special.Of course, sum types go beyond errors, and open other avenues that are very poorly served by Go right now.
---
† This, unfortunately, isn't universally true. For example, io.Reader's Write() method's contract says that if it returns io.EOF, it may still have read a final amount of data into its buffer. Many get this wrong and ignore the data read. Which points to the problem of conventions that only seem like unwritten rules, but also highlights how the lack of strong type-system support creates the issue in the first place.
Error handling in go is not concise, and calling it explicit is in insult to anyone with a modicum of abstract reasoning skills. Error handling in go is not explicit, it's verbose. Error handling in go is not concise, it's broad. Error handling in Go is not consistent, it's an exercise left up to diligent programmers.
As golang's code base grows, you're going to see this mistake play out over and over and over again.
Having a Result<T> construct would have been great for go.
If you don't like writing `if err!=nil {...}` throughout your code base surely you can just create some middleware function in its own package that has switch statements based on error cases. Checking for errors frequently in the running of the code is more or less a Good Thing.
So you can do (sans syntax):
ErrorCheckerFunc(fn myParams -> MyFunc(myParams), "message")
ErrorCheckerFunc(fn myParams2 -> MyFunc2(myParams2), "message2")
etc...and then the actual function:
ErrorCheckerFunc(fn myFunc, message){
returnVal, returnError = myFunc();
switchError:
case error is foo:
print error
case error is bar:
log error print "error logged"
...
case n+1 etc
// you can handle message in a similar way to get
// cases by function names, and if you have elixir
// style quote unquote metaprogramming in Golang you might just be able to get function names based on passed function - I don't think this is in the language spec though it may be possible to do some other way.
if error not nil do switchError(error)
return RuhRoh
else
return returnVal
}
It's not so bad, it's just a middleware.Alas, that's obviously completely impossible. Such a thing is beyond the capabilities of us mortal programmers. But maybe one day PL researchers will discover a way. One day...
Most of the time the receiver is swallowing the error or spitting it to stdout, so why bother
Or maybe she took the approach Linus did with git and just let it crap out and start clean next big bang.