Errors in Go: From denial to acceptance(evilmartians.com)
evilmartians.com
Errors in Go: From denial to acceptance
https://evilmartians.com/chronicles/errors-in-go-from-denial-to-acceptance
25 comments
Go 2 is likely to get a per-call variation of try/catch via named handle blocks, see:
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback
and this broad survey of error handling requirements:
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f...
Also the author overlooks the Error Values draft design, which is tentatively targeted for Go 1.13:
https://go.googlesource.com/proposal/+/master/design/go2draf...
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback
and this broad survey of error handling requirements:
https://gist.github.com/networkimprov/961c9caa2631ad3b95413f...
Also the author overlooks the Error Values draft design, which is tentatively targeted for Go 1.13:
https://go.googlesource.com/proposal/+/master/design/go2draf...
I believe handle blocks is exactly what they were referring to as "basically a reverse try-catch at best and a Visual Basic “on error goto” at worst".
Based on the feedback page linked above, I believe what is likely for Go 2 is quite different than "check/handle". See the requirements page linked above for more.
Without taking sides on the issue, these are purposeful trade-offs that the Go team makes. They are trying to avoid bloating the language and making it too difficult for them to add optimizations or other features. And I think they would argue that it makes ignoring errors harder, since you have to intentionally discard the error value when it was given to you, which takes conscious effort, rather than omitting it from the code completely like other languages let you.
> these are purposeful trade-offs that the Go team makes.
Trade-offs for whom or with what in mind? the compiler or the programmer?
That's the "philosophical" difference between the Rust team and the Go team.
Trade-offs for whom or with what in mind? the compiler or the programmer?
That's the "philosophical" difference between the Rust team and the Go team.
The cynic in me says it's optimizing to hire lots of low-skill programmers at Google for cheap.
Exactly. That is why Rust does so many more things, but Go compiles so fast.
Even with two wildly different approaches to language design, each has their place, and their own set of drawbacks.
Even with two wildly different approaches to language design, each has their place, and their own set of drawbacks.
It's a false dichotomy. Languages like Java or Kotlin compile fast and have exceptions too.
Because the JVM takes another tradeoff against Go and Rust: it has a runtime interpreter+JIT, which causes even more performance overhead than Go's GC let alone Rust which - at runtime - is effectively a zero-cost abstraction over C and jemalloc.
The JVM GCs generally have much lower overhead than Go's does. Go imposes enormous collection costs on apps to try and drive latency so low. This is widely acknowledged in discussions of the various GC tradeoffs.
The GCs alone? Yes. The rest of their respective runtime architectures? Highly doubtful. As GC-dependent as Go may be, it still does end up as native code.
But frankly, comparing Go to Java when things like Rust exist ... seems analogous to comparing the z80 to the AVR when 32-bit ARM cores exist.
But frankly, comparing Go to Java when things like Rust exist ... seems analogous to comparing the z80 to the AVR when 32-bit ARM cores exist.
Offtopic/meta: for some reason your post was dead. The comment at the end is kind of pointless and flamey, Rust isn't that good, but doesn't seem to justify a post kill. I vouched for you to bring it back.
Not quite - Java is faster than Kotlin [0] but I've never seen a Golang app take more than 10 seconds (and that's something huge like Kubernetes or Docker components)
[0] https://medium.com/keepsafe-engineering/kotlin-vs-java-compi...
[0] https://medium.com/keepsafe-engineering/kotlin-vs-java-compi...
The article you link comes to a different conclusion:
> With the Gradle daemon running and incremental compilation turned on, Kotlin compiles as fast or slightly faster than Java.
> With the Gradle daemon running and incremental compilation turned on, Kotlin compiles as fast or slightly faster than Java.
Go optimizes for fast compilation, as such the language has fewer internal checks for only the absolutely necessary. In my opinion, to be able to continue execution when an error happened earlier could be a blessing or a curse, and it depends on the programmer. I've been writing Go for 2 years now and I don't have a single case where I accidentally ignored an error. I have ignored errors, but usually for operations whose end result I don't care about or doesn't affect what happens next in the program.
There's also a recommended way of dealing with errors. I may not state it correctly but basically you build a pipeline of operations for a data type that represents the arguments (or data) of the operations. For example, to compress an image given a URL:
There's also a recommended way of dealing with errors. I may not state it correctly but basically you build a pipeline of operations for a data type that represents the arguments (or data) of the operations. For example, to compress an image given a URL:
type Image struct {
src string
bytes []byte
width uint
height uint
err error
}
func NewImage(src string) Image {
return Image{src: src}
}
func (img Image) Get() {
if err != nil {
return
}
...
// Set error value if this operation fails
}
func (img Image) Compress {
if err != nil {
return
}
...
// Set error value if this operation fails
}
func (img Image) Err() error { return img.err }
img := NewImage("https://image.src/random-image.png")
img.Get()
img.Compress()
if img.Err() != nil {
// handle error just once
}this does work... but it also requires two very significant points.
1: you must do this wrapping yourself, for everything you wish to simplify, as few libs do it.
2: you now have non-standard error handling and your tools will not warn you if you handle it wrong.
the second one, to me, is borderline fatal for this pattern. You can't look at this code and realize it's missing error handling (or doing it incorrectly), and you can't run `errcheck` to tell you that e.g. you're missing `err := img.Get()`.
1: you must do this wrapping yourself, for everything you wish to simplify, as few libs do it.
2: you now have non-standard error handling and your tools will not warn you if you handle it wrong.
the second one, to me, is borderline fatal for this pattern. You can't look at this code and realize it's missing error handling (or doing it incorrectly), and you can't run `errcheck` to tell you that e.g. you're missing `err := img.Get()`.
And all the code to satisfy borrow checker in Rust, is it for compiler or programmer?
That’s like asking “are type signatures for the compiler, or the programmer?” The answer is a bit of both: for the programmer to express intent, and for the compiler to check that intent is correct.
Why would a programmer have an intent about types to express unless s/he had been trained to think of programming in terms of static typing?
If your type system is powerful enough (like in Rust), then you can use it to express relations in your problem domain and have compiler enforce them for you before the code even runs.
Example: https://blog.chain.com/bulletproof-multi-party-computation-i...
Example: https://blog.chain.com/bulletproof-multi-party-computation-i...
I think you missed the "unless" in my question. I am well aware that having chosen to treat types as a first class problem, a coder can leverage that intent. My point is simply that there is a populous, productive world of software development in which typing is a humdrum issue for the coders. C.f. the last 30 years of Javascript and Python
What makes that an interesting question? That’s like saying “how would someone have an idea to tests their code unless they were trained to think about testing?” It feels like a tautology, and not particularly relevant.
(I love both dynamic and static typing. I think dynamic type systems are more useful than basic static type systems, but also really enjoy more expressive static type systems.)
(I love both dynamic and static typing. I think dynamic type systems are more useful than basic static type systems, but also really enjoy more expressive static type systems.)
Perhaps the programmer should be focused on the purpose the code serves, rather than the code as code object. Mental cycles devoted to thinking about types are, for most programs, overhead. It's perfectly reasonable to be a strong static-typing believer, though I am not. But it's also perfectly reasonable not to think about types when coding, as millions of useful programs have demonstrated.
FWIW, I've found many people new to programming expect something like types, even though they don't have the vocabulary for it yet. It really just depends on the individual.
Sure. And many people new to skiing expect something like gussets to keep the snow out of their underwear when they fall. Issue is whether skiing is about clothing or moving over the landscape. Some focus on your clothes is appropriate, and more in certain circumstances. Rust, which I see you are a proselytizer for (I was at Mozilla during Rust's ramp-up, FWIW), is a tool for writing code that is closely integrated with the operating system. In that context, a type-centric mindset is necessary and valuable. If you are writing code in other contexts: exploratory data analysis, a simple Android app, and on and on, then types are there, but programming is not type management
Sure, that's why both kinds are useful. I don't disagree at all.
What do you mean by types? Most modern languages have fairly extensive type systems.
Typing doesn't have to be static (or strong) to be there. When I write `a + b`, I intend `a` and `b` to be things such that the `+` operator makes sense for them. Might be two numbers, might be two strings, might be a string and a number if I'm using js/php, the point is that I always have an intention, implicit or explicit.
"When I walk to the store, I intend for the ground to hold me up. My intention might be implicit, but it is always there."
If it would bring significant amount of technical debt to introduce some synctactic sugar that'd allow to bubble errors up the call stack without 3 extra lines of code, then design choices made for Go seem to be questionable. At least superficially.
I find this perspective curious. Maybe it's the non-web field that I work in, but I've never written a program where I could unintentionally ignore errors and have things go as expected after.
What are some cases where errors can be ignored? And if they can be, are these neccesarily some sort of "status" rather than "error"?
What are some cases where errors can be ignored? And if they can be, are these neccesarily some sort of "status" rather than "error"?
Well look at: https://golang.org/pkg/database/sql/#DB.Query
How likely is the error going to happen, if you know (through tests and everything) that the query is correct?
it can only happen in two scenarios:
1. the database has serious problems 2. the driver is incorrect
in normal cases you would probably handle that with a middleware in classical languages. i.e. you would have a middlewre that catches exceptions and handle the error there (logging, paging, whatever) and maybe show a nice looking "we are experience problems now"-page.
in golang this is a little bit harder, since you would need to handle the error by every caller and with the default http interface you would actually need to call your "handle default error" in every http handler.
How likely is the error going to happen, if you know (through tests and everything) that the query is correct?
it can only happen in two scenarios:
1. the database has serious problems 2. the driver is incorrect
in normal cases you would probably handle that with a middleware in classical languages. i.e. you would have a middlewre that catches exceptions and handle the error there (logging, paging, whatever) and maybe show a nice looking "we are experience problems now"-page.
in golang this is a little bit harder, since you would need to handle the error by every caller and with the default http interface you would actually need to call your "handle default error" in every http handler.
If you're writing a command line utility that tries to load a config file and falls back on defaults if it's not found, you may try to load it and just ignore any error, only caring that it either returns a path if found or null if you should use default values.
I believe what you are saying is that you can ignore errors if you check or rely on the side effects of an error (such as the result of a function being null on error).
One could argue that the side effect is part of the error, and really what you are ignoring are the details of an error (e.g. did the config entry fail to load because the config file wasn't found, because of filesystem permissions, or because the particular configuration key wasn't present?)
One could argue that the side effect is part of the error, and really what you are ignoring are the details of an error (e.g. did the config entry fail to load because the config file wasn't found, because of filesystem permissions, or because the particular configuration key wasn't present?)
That's a good way to put it, and yeah I would agree that's how I like to use errors: give me the details I ask for and let me ignore the ones I don't care about. Sometimes an "error" isn't really an error, and at those times exceptions are overkill. Sometimes an error really is an error and I need to let the user know something went wrong, give them enough details to let them fix it and wait for them to try again.
But wouldn't that look like:
var s Settings
if s, err := load_settings(); err != nil {
s = defaults()
}
Where is the unhandled error?Note that your code will not work; you've shadowed 's' inside the if block because you have 'if s, err := ...'. As a result, 's = defaults()' will set the shadowed 's' variable that only exists within the if block to defaults, but the 'var s Settings' you declared above will not be modified.
Here's a playground showing the issue: https://play.golang.org/p/eE0HEZx1MJu
Go is full of nice little foot-guns like that :)
You would want to also have 'var err error' above the block and use '=' instead of ':=' to fix this sort of issue.
Here's a playground showing the issue: https://play.golang.org/p/eE0HEZx1MJu
Go is full of nice little foot-guns like that :)
You would want to also have 'var err error' above the block and use '=' instead of ':=' to fix this sort of issue.
I am by no means an expert (or even a frequent user) of go. However, the most unfortunate foot cannon I have encountered is this:
package main
type HowOdd interface {
Poke() string
}
type AConcreteThing struct {
}
func (t *AConcreteThing) Poke() string { return "hehe" }
func DoAConcreteThing() *AConcreteThing {
return nil
}
func DoAThing() HowOdd {
return DoAConcreteThing()
}
func main() {
lolWhat := DoAThing()
if lolWhat != nil {
panic("How could this happen?")
}
}
I understand that the interface at the bottom is a non-null interface containing a null value, but...damn, that sucks.[deleted]
An example is logging something to a remote server for analytics purposes: you can fire and forget.
I agree with you, but it's important to acknowledge the strength of the argument against exceptions. I think Raymond Chen put forth some of the best arguments, claiming they were human error prone:
https://blogs.msdn.microsoft.com/oldnewthing/20050114-00/?p=...
https://blogs.msdn.microsoft.com/oldnewthing/20040422-00/?p=...
While forceful, I don't accept the conclusion of Raymond's argument. I feel it may be true for programmers primarily trained and experienced in C, but is not true in general. As such, the argument reduces to "programmers who are not familiar with exceptions sometimes make mistakes when using them." One could say the same thing about pointers, dynamic typing, lambdas, macros, comprehensions, pass-by-reference, etc. Today, most programmers are familiar and comfortable with exceptions, having almost certainly been taught to use them in college and almost certainly using them on a daily basis for their entire professional career. They'll also have been taught to use a language-specific toolbox such as RAII or try/catch/finally to deal with any potential problems.
Once you get in the habit, it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?" and to take appropriate precautions. In fact, I'd wager that for anyone who's spent years working in any language with exceptions this quickly becomes second nature. It's also an easy way to add value during peer code review. In my experience, writing correct code with exceptions is not "really hard," as Raymond argues, but rather no harder and a lot quicker that using explicit error handling. The reason it's a lot quicker is because "exit the function with an error status and do local cleanup of variables initialized so far" is by far the most common correct action. And for cases when its not, the try/catch/finally or other exception handling whatever is straight forward to write and no uglier than explicit error handling would have been. Of course, it's possible I think this because I've worked in languages with exceptions for most of my career. But I certainly don't observe that even the most junior programmers on my team create a ton of bugs that could be attributed to mishandled exceptions. In fact, I can only remember a handful of such bugs, mostly involving open database transactions or unhelpful error messages masking more detailed error messages.
A more solid objection to language support for exceptions can be made by pointing out all invisible code generated by the compiler to correctly handle exceptions. This can lead to significant code bloat and in some cases can negatively impact performance. Or by pointing out how unwise it is to be allocating additional Exception objects while dealing with out of memory errors. These are some of the reasons why C++ isn't appropriate for an OS kernel, for example.
https://blogs.msdn.microsoft.com/oldnewthing/20050114-00/?p=...
https://blogs.msdn.microsoft.com/oldnewthing/20040422-00/?p=...
While forceful, I don't accept the conclusion of Raymond's argument. I feel it may be true for programmers primarily trained and experienced in C, but is not true in general. As such, the argument reduces to "programmers who are not familiar with exceptions sometimes make mistakes when using them." One could say the same thing about pointers, dynamic typing, lambdas, macros, comprehensions, pass-by-reference, etc. Today, most programmers are familiar and comfortable with exceptions, having almost certainly been taught to use them in college and almost certainly using them on a daily basis for their entire professional career. They'll also have been taught to use a language-specific toolbox such as RAII or try/catch/finally to deal with any potential problems.
Once you get in the habit, it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?" and to take appropriate precautions. In fact, I'd wager that for anyone who's spent years working in any language with exceptions this quickly becomes second nature. It's also an easy way to add value during peer code review. In my experience, writing correct code with exceptions is not "really hard," as Raymond argues, but rather no harder and a lot quicker that using explicit error handling. The reason it's a lot quicker is because "exit the function with an error status and do local cleanup of variables initialized so far" is by far the most common correct action. And for cases when its not, the try/catch/finally or other exception handling whatever is straight forward to write and no uglier than explicit error handling would have been. Of course, it's possible I think this because I've worked in languages with exceptions for most of my career. But I certainly don't observe that even the most junior programmers on my team create a ton of bugs that could be attributed to mishandled exceptions. In fact, I can only remember a handful of such bugs, mostly involving open database transactions or unhelpful error messages masking more detailed error messages.
A more solid objection to language support for exceptions can be made by pointing out all invisible code generated by the compiler to correctly handle exceptions. This can lead to significant code bloat and in some cases can negatively impact performance. Or by pointing out how unwise it is to be allocating additional Exception objects while dealing with out of memory errors. These are some of the reasons why C++ isn't appropriate for an OS kernel, for example.
> Once you get in the habit, it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?" and to take appropriate precautions.
I feel there's a serious disagreement here with how I tend to look at things. I don't want to look line-by-line and ask what would happen in case of an exception. In most cases the answer would be: That would be bad, since the rest of the code would not get executed and the program would remain in an invalid state.
It's much easier to know that there will not be an exception. Simple sequential code, the way it was intended to be by the ancient gods. And handle errors manually where they could in fact occur. I reckon that for my own code, that is only once in a hundred lines or more. Or maybe even more (lines) than that, since I'll abort directly in the callee in many cases anyway, so the caller never gets a chance to see the error. But if there were any point in handling an error in the future, we can still switch to handling it later.
Now you can argue that you can do that with exceptions as well (handling errors directly either by returning an error code or by aborting) such that exceptions would never bubble up. But in that case what's the point in exception handling to begin with? It's just the same now, except that there's a lot of exceptions machinery required in the language.
I feel there's a serious disagreement here with how I tend to look at things. I don't want to look line-by-line and ask what would happen in case of an exception. In most cases the answer would be: That would be bad, since the rest of the code would not get executed and the program would remain in an invalid state.
It's much easier to know that there will not be an exception. Simple sequential code, the way it was intended to be by the ancient gods. And handle errors manually where they could in fact occur. I reckon that for my own code, that is only once in a hundred lines or more. Or maybe even more (lines) than that, since I'll abort directly in the callee in many cases anyway, so the caller never gets a chance to see the error. But if there were any point in handling an error in the future, we can still switch to handling it later.
Now you can argue that you can do that with exceptions as well (handling errors directly either by returning an error code or by aborting) such that exceptions would never bubble up. But in that case what's the point in exception handling to begin with? It's just the same now, except that there's a lot of exceptions machinery required in the language.
Is there such thing as 'simple sequential code'?
Simple? Additions may overflow, printf may fail, etc. Usually code looks simple only because it ignores errors..
As for sequential, we're in the age of multiple cores and GPUs..
Simple? Additions may overflow, printf may fail, etc. Usually code looks simple only because it ignores errors..
As for sequential, we're in the age of multiple cores and GPUs..
Of course, sequential code is simple. Overflowing additions don't have anything todo with the code. They overflow or not.
> Usually code looks simple only because it ignores errors..
No, it looks simple when the structure is right. There are other error handling strategies than "handle it now, or pop the stack frame and let your caller handle it now!". But with exceptions that's all the choice you are given (unless you convert them to regular data).
Since exceptions strongly discourage alternative error handling schemes, no wonder they lead to unmaintainable code as soon as a considerable amount of conditions actually should be handled, instead of just bailing out.
> As for sequential, we're in the age of multiple cores and GPUs..
What point are you trying to make? Obviously what I mean by sequential code here is that you can easily reason "if control flow reaches point A, then B will be executed, too".
> Usually code looks simple only because it ignores errors..
No, it looks simple when the structure is right. There are other error handling strategies than "handle it now, or pop the stack frame and let your caller handle it now!". But with exceptions that's all the choice you are given (unless you convert them to regular data).
Since exceptions strongly discourage alternative error handling schemes, no wonder they lead to unmaintainable code as soon as a considerable amount of conditions actually should be handled, instead of just bailing out.
> As for sequential, we're in the age of multiple cores and GPUs..
What point are you trying to make? Obviously what I mean by sequential code here is that you can easily reason "if control flow reaches point A, then B will be executed, too".
> > As for sequential, we're in the age of multiple cores and GPUs..
> What point are you trying to make? Obviously what I mean by sequential code here is that you can easily reason "if control flow reaches point A, then B will be executed, too".
That goes out the door with any parallelism whatsoever, because control flow may reach point A in thread 1, but then thread 2 kills thread 1, and point B is never reached.
It's increasingly hard to bury ones head in the sand and pretend every machine is a single-CPU single-task system with no interrupts.
> What point are you trying to make? Obviously what I mean by sequential code here is that you can easily reason "if control flow reaches point A, then B will be executed, too".
That goes out the door with any parallelism whatsoever, because control flow may reach point A in thread 1, but then thread 2 kills thread 1, and point B is never reached.
It's increasingly hard to bury ones head in the sand and pretend every machine is a single-CPU single-task system with no interrupts.
Sure, today's CPUs execute not only in parallel but also out-of-order. But that doesn't matter since threads don't let threads die before they've done their share.
Out-of-order makes thread synchronization a little harder (when implementing lock-free algorithms for example), but that has nothing to do with exceptions.
Out-of-order makes thread synchronization a little harder (when implementing lock-free algorithms for example), but that has nothing to do with exceptions.
> and the program would remain in an invalid state.
But this is only ever possible if the program moved to an invalid state in the first place. Granted, sometimes there is no way around that, but usually it can and should be avoided.
But this is only ever possible if the program moved to an invalid state in the first place. Granted, sometimes there is no way around that, but usually it can and should be avoided.
For the programs I write, I'm sure the majority of function invocations do only a partial transformation (i.e., result in an invalid state).
> it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?"
I once implemented a C++ class where some member functions implemented the strong exception safety guarantee. I can tell you, yes it really is that hard. Even when you know certain variables are pointers and can't throw, you are tiptoeing around on razorblades.
At my day job we use exceptions in Java a lot, and it's a good abstraction for rolling back database transactions. But for non-transactional code it isn't the best.
I once implemented a C++ class where some member functions implemented the strong exception safety guarantee. I can tell you, yes it really is that hard. Even when you know certain variables are pointers and can't throw, you are tiptoeing around on razorblades.
At my day job we use exceptions in Java a lot, and it's a good abstraction for rolling back database transactions. But for non-transactional code it isn't the best.
> I feel it may be true for programmers primarily trained and experienced in C, but is not true in general.
If you're mentally comparing against the way error handling in C, your position is much more defensible. Error handling in C requires manually releasing resources in the correct order, and it's easy for a destructor to slip by in the wrong order, to get missed entirely, or for someone to use the wrong jump label. (I'm thinking about Linux kernel style code, here.) If I'm reviewing some error handling code in C, I might have to scan up to the top of the function and count all the resources which need to be released, then look down to the bottom to see that the right label is being jumped to. That's a pain.
In languages like Golang, error handling and resource cleanup are a bit more orthogonal. You can look at a Golang call which returns an error and ask, "Does this handle the error correctly?" Usually this only requires looking at three lines of code at a time, maybe four. Resources are just released with defer, except in unusual cases.
> Once you get in the habit, it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?"
I guess I never got in the habit. Maybe you have better habits than I do, or maybe I'm just not "smart enough" to be a programmer, but I find this hard, so I choose to use languages which make this easier. I find it hard in Python, hard in JavaScript, hard in C++, and hard in C#. I'm really not even sure how to e.g. handle exceptions thrown by C++ constructor failures separately from other things later in the block, e.g., I want to do this:
> In fact, I'd wager that for anyone who's spent years working in any language with exceptions this quickly becomes second nature.
Based on what I see during code reviews, I don't agree. Raymond Chen has a good point... when I review someone's Golang code, checking for correct error handling is fast and easy, it's mostly mechanical, easy to target with lint, and I can immediately move on to looking at the code semantics. With, say, C# code, or especially Python code, there are too many cases where a method call slips inside or outside the try/catch block where it belongs.
I've also been bitten too many times by bugs in production where a function throws an exception, and we have to decode the stack trace to figure out what the error means or how the error is even possible. This is typically easier in programs written in Golang, at least in my experience.
The more syntactic sugar a language has, the more likely it is that I don't notice an error condition that is being handled incorrectly. Golang, with its simplicity, makes you do some extra typing up front but in my experience it pays for itself by going faster through code review and bug fixes.
This depends on the kind of code you're writing. If you're mostly sticking inside application memory and not doing, e.g., IO, IPC, etc, then the exception approach is better.
I'm not saying Golang is perfect by any means, but I find the way it does error handling to be a welcome reduction in my cognitive load.
If you're mentally comparing against the way error handling in C, your position is much more defensible. Error handling in C requires manually releasing resources in the correct order, and it's easy for a destructor to slip by in the wrong order, to get missed entirely, or for someone to use the wrong jump label. (I'm thinking about Linux kernel style code, here.) If I'm reviewing some error handling code in C, I might have to scan up to the top of the function and count all the resources which need to be released, then look down to the bottom to see that the right label is being jumped to. That's a pain.
In languages like Golang, error handling and resource cleanup are a bit more orthogonal. You can look at a Golang call which returns an error and ask, "Does this handle the error correctly?" Usually this only requires looking at three lines of code at a time, maybe four. Resources are just released with defer, except in unusual cases.
> Once you get in the habit, it's not hard to scan through a program line-by-line asking yourself "what would happen if an exception were thrown at this point?"
I guess I never got in the habit. Maybe you have better habits than I do, or maybe I'm just not "smart enough" to be a programmer, but I find this hard, so I choose to use languages which make this easier. I find it hard in Python, hard in JavaScript, hard in C++, and hard in C#. I'm really not even sure how to e.g. handle exceptions thrown by C++ constructor failures separately from other things later in the block, e.g., I want to do this:
try {
MyType x{args, args};
} catch (SomeException &e) {
...
}
x.DoSomething();
And I'm not sure how to even write this as functional C++ without doing something weird or maybe just saying "fuck it" and wrapping x in std::unique_ptr.> In fact, I'd wager that for anyone who's spent years working in any language with exceptions this quickly becomes second nature.
Based on what I see during code reviews, I don't agree. Raymond Chen has a good point... when I review someone's Golang code, checking for correct error handling is fast and easy, it's mostly mechanical, easy to target with lint, and I can immediately move on to looking at the code semantics. With, say, C# code, or especially Python code, there are too many cases where a method call slips inside or outside the try/catch block where it belongs.
I've also been bitten too many times by bugs in production where a function throws an exception, and we have to decode the stack trace to figure out what the error means or how the error is even possible. This is typically easier in programs written in Golang, at least in my experience.
The more syntactic sugar a language has, the more likely it is that I don't notice an error condition that is being handled incorrectly. Golang, with its simplicity, makes you do some extra typing up front but in my experience it pays for itself by going faster through code review and bug fixes.
This depends on the kind of code you're writing. If you're mostly sticking inside application memory and not doing, e.g., IO, IPC, etc, then the exception approach is better.
I'm not saying Golang is perfect by any means, but I find the way it does error handling to be a welcome reduction in my cognitive load.
How is decoding a stack trace easier for Go programs? My experience has been that Go programs simply don't generate stack traces when something goes wrong. Instead you get some log message with a generic "Something went wrong in subsystem X" type message because the error codes got passed up and abstracted by hand. Exceptions with stack traces are far, far superior to that when it comes to rapidly identifying and fixing faults.
In Golang you will typically annotate the error messages with meaningful context as you go up the stack. By comparison, my experience is that often, exceptions only have the stack and one error message, and are missing critical pieces of context. The stack trace will only contain the names of functions, and it will often contain the names of functions that are irrelevant to debugging.
A good error message might look like this:
Exceptions optimize for the case where the exception bubbles straight up to some handler, but I'd rather not have my errors work that way. It's incredibly common for it to make no sense to bubble an error up without some kind of annotation or additional context.
A good error message might look like this:
Could not travel to Alpha Centauri:
warp drive initialization failed:
power coupling 397 is offline:
temperature out of range (temp = 150°C)
A bad stack trace might look like this: DeviceClient::Connect()
std::promise::something
<anonymous function>
DeviceSet::PowerOnAllDevices()
<anonymous function>
std::etcetera
std::etcetera<some_big_thing, more_params, allocator=std::allocator>
TravelCommand::Engage()
VoiceCommand::RunCommand()
Error: temperature out of range (temp = 150°C)
Stack traces are useful, but they're a poor substitute for good error messages. There's just too much context missing from raw stack traces (which file, which URL, which device) and too much noise (anonymous functions, higher-order functions, etc.) That, and a stack trace often requires reading the source code to interpret properly. That might not be possible or it might be outside the skill set of the person interpreting the error.Exceptions optimize for the case where the exception bubbles straight up to some handler, but I'd rather not have my errors work that way. It's incredibly common for it to make no sense to bubble an error up without some kind of annotation or additional context.
This doesn't seen like a particularly fair comparison because bad error messages (if we are to compare them to a bad stack trace) look like this:
I'm not sure if you're trying to make some kind of joke. An empty string is a bad error message, but you would have to go out of your way to make this happen in Go, you would have to do something obviously wrong like
Stack traces are not like this, you can't really control whether they are good or bad, at least not well. If you want to make your stack traces more informative what do you do? You can't really change them, they just reflect your call graph.
Golang optimizes for the case where you get good error messages. Exceptions optimize for the case where you don't care about adding context to the error and just want to bubble it upwards. On the balance of things, I prefer Golang's approach because most of the time, I don't want to see the stack trace.
return errors.New("")
You have control over whether the error messages are good or bad, you just have to insert the right annotations where the critical pieces of context are known.Stack traces are not like this, you can't really control whether they are good or bad, at least not well. If you want to make your stack traces more informative what do you do? You can't really change them, they just reflect your call graph.
Golang optimizes for the case where you get good error messages. Exceptions optimize for the case where you don't care about adding context to the error and just want to bubble it upwards. On the balance of things, I prefer Golang's approach because most of the time, I don't want to see the stack trace.
Exceptions in many languages carry messages and exceptions can be nested. There's also nothing stopping you from logging messages as you handle exceptions. The best case of exceptions is easily as good as best case go-style error handling and reporting. The worst case of exceptions is far more informative than the worst case of error messages. I'm not sure what an example that requires great use of some language facility is supposed to be illustrative of - good use of a facility is usually... good?
[Edit: this was a reply to a comment you seem to have rewritten (I think for the worse) but I think the response still mostly works]
[Edit: this was a reply to a comment you seem to have rewritten (I think for the worse) but I think the response still mostly works]
I think this can fundamentally be summed up as:
errors are "failure as data" (hence, message), and you get to write the flow (including handling and annotating the call stack)
vs
exceptions are "failure as flow" (hence, stack trace), and you get to write the data (including logging)
May very well be paraphrasing a well known maxim about closures and objects as errors are a poor man's exceptions and exceptions are a poor man's errors.
errors are "failure as data" (hence, message), and you get to write the flow (including handling and annotating the call stack)
vs
exceptions are "failure as flow" (hence, stack trace), and you get to write the data (including logging)
May very well be paraphrasing a well known maxim about closures and objects as errors are a poor man's exceptions and exceptions are a poor man's errors.
try {
MyType x{args, args};
x.DoSomething();
} catch (SomeException &e) {
...
}
You can't do much with the x if it didn't construct correctly, so you either need a strategy for a safe default value for x, or you need to bail entirely if an exception occurs.The author seems to generalise his use case into a description of how to use panic/recover as a bespoke exception mechanism. In that case this is still bargaining.
> In imgproxy, I use this approach to stop image processing if the timeout is reached (see here, and here). The goal is not to bother about returning timeout errors from each function
(emphasis mine)
While I understand the pain, I do not like the train of thought. It is our duty as responsible developers to bother with error handling. Luckily this is not a library but a "standalone application", not a "library", which is a slightly different use case (e.g negroni has to recover from panics to log and continue serving http requests).
The accepted solution looks like† a world of pain waiting to blow up in a myriad of subtle corner cases (do you remember what happens exactly when there's a panic in a defer that was called because of a panic, and in which order deferred functions are called?) and is barely more readable. I've seen much more interesting and obviously robust Go 1 patterns (that I can't find right now) that e.g pass error handler functions around.
† At first sight. Maybe it's not when digging deeper, but that's not the point: the most glorious thing to me when I read idiomatic Go code is that basically everything is boringly obvious. This is a clever hack and crosses a threshold I'm not willing to go past in production-class code.
> In imgproxy, I use this approach to stop image processing if the timeout is reached (see here, and here). The goal is not to bother about returning timeout errors from each function
(emphasis mine)
While I understand the pain, I do not like the train of thought. It is our duty as responsible developers to bother with error handling. Luckily this is not a library but a "standalone application", not a "library", which is a slightly different use case (e.g negroni has to recover from panics to log and continue serving http requests).
The accepted solution looks like† a world of pain waiting to blow up in a myriad of subtle corner cases (do you remember what happens exactly when there's a panic in a defer that was called because of a panic, and in which order deferred functions are called?) and is barely more readable. I've seen much more interesting and obviously robust Go 1 patterns (that I can't find right now) that e.g pass error handler functions around.
† At first sight. Maybe it's not when digging deeper, but that's not the point: the most glorious thing to me when I read idiomatic Go code is that basically everything is boringly obvious. This is a clever hack and crosses a threshold I'm not willing to go past in production-class code.
I completely agree. I think people are shocked, at first, at how everything can go wrong in ways they've never considered. Exceptions hide the complexity by pretending that errors are exceptional things that will never happen to YOU. The opposite is what's true, though, random errors will happen all the time; networks drop packets, remote machines are slow, database transactions fail because of other writers, etc. If you are aware of all the things that can go wrong, you can handle them. If you treat them as "exceptions" rather than the rule, then you will just write flaky software that needs constant manual attention. Some people like this, I guess, but I'm personally not a fan.
The syntax could be better (I do have err, !=, and nil keys on my keyboard, I really do) and it would be nice to annotate errors and not lose the semantic information (fmt.Errorf("trying foo: %v", err) throws away the specifics of err beyond the result of err.Error()), but really... explicitly handling errors after every function, even if it's just "return err" and punt to the code one level up... is something you have to do in every language. Go just front-loads it.
As for timeouts, I am not sure why you wouldn't just pass in a context, which already has provisions for a deadline, explicit cancellation, and cancelling further library calls. If you can cancel your operation when <-ctx.Done() returns, then you can ensure that all the related work is cancelled. You don't need a clever panic/recover for that. Just select { case <-workDone: ...; case <-ctx.Done(): cancelWork(); return ctx.Err() }. I am not sure why people rely on hacks when there's already a standard method built into the language.
The syntax could be better (I do have err, !=, and nil keys on my keyboard, I really do) and it would be nice to annotate errors and not lose the semantic information (fmt.Errorf("trying foo: %v", err) throws away the specifics of err beyond the result of err.Error()), but really... explicitly handling errors after every function, even if it's just "return err" and punt to the code one level up... is something you have to do in every language. Go just front-loads it.
As for timeouts, I am not sure why you wouldn't just pass in a context, which already has provisions for a deadline, explicit cancellation, and cancelling further library calls. If you can cancel your operation when <-ctx.Done() returns, then you can ensure that all the related work is cancelled. You don't need a clever panic/recover for that. Just select { case <-workDone: ...; case <-ctx.Done(): cancelWork(); return ctx.Err() }. I am not sure why people rely on hacks when there's already a standard method built into the language.
The problem with this attitude is that it ... it's just not true. C-style not true.
C, like Go, convinces people they handle errors. And then, when you look into what that actually means, correctly handling error cases for even trivial problems ... you immediately find out "nope, you're not handling errors, you're ignoring them". C, and Go are very sneaky that way.
1) one of the more common error cases is nil, closely followed by out of memory and zero division ... which in Go causes a panic just like every other language.
So your Go error handling in this case is never going to be executed. If you think you can avoid exceptions, you're lying to yourself. Every last method needs to be ready that a panic gets called at almost any point. Which ... is exactly what the error system was supposed to fix.
2) "If you treat them as "exceptions" rather than the rule, then you will just write flaky software ..."
This sounds nice until you look at Go sources on github. And you see WHY they're not flaky. Do people handle errors more in Go sources than in Java ? The reverse is true !
But Go software is more stable. How can this happen ?
Well, Go defaults to ignoring errors and just continuing whereas Java (and every sane language) defaults to aborting the program rather than letting it run in an unknown state.
To put it more extreme, and more to the point Go just starts "sudo rm -Rf /" when an error happens. The reality is that Go programs just start writing things to the database based on wrong information when an error happens.
3) "As for timeouts, I am not sure why you wouldn't just pass in a context, which already has provisions for a deadline, explicit cancellation, and cancelling further library calls..."
Again, this sounds cool. You can recognize code that actually uses this correctly.
Incorrect (in more ways than one):
And what will this do ?
1) it may actually work (one hopes, the common case)
2) it may not write anything
3) it may PARTIALLY write what you asked to be written (including not writing anything at all and returning EAGAIN, which is why the retry logic is not really optional. Frankly you should have a higher max_retries, and ignoring for the EAGAIN case to avoid some very rare circumstances)
4) it may panic
5a) it may block for a long time
5b) it may block forever (and even make your program unkillable. I mean, we've all used NFS, right ?)
What will be your total amount of material to diagnose this problem when it occurs in cases 1-3 ? Nothing whatsoever.
Case 4 ? A large collection of stacktraces. Thankfully usually with the relevant one on top.
5a and 5b ? 100 stacktraces, when you finally kill it (or ... well Go "supports hundreds of thousands goroutines"), one of which is relevant. Well the truth is that it may actually have so many goroutines that it ... well I managed to run out of diskspace once. But it's easily 10 megabytes and more in a real webserver.
C, like Go, convinces people they handle errors. And then, when you look into what that actually means, correctly handling error cases for even trivial problems ... you immediately find out "nope, you're not handling errors, you're ignoring them". C, and Go are very sneaky that way.
1) one of the more common error cases is nil, closely followed by out of memory and zero division ... which in Go causes a panic just like every other language.
So your Go error handling in this case is never going to be executed. If you think you can avoid exceptions, you're lying to yourself. Every last method needs to be ready that a panic gets called at almost any point. Which ... is exactly what the error system was supposed to fix.
2) "If you treat them as "exceptions" rather than the rule, then you will just write flaky software ..."
This sounds nice until you look at Go sources on github. And you see WHY they're not flaky. Do people handle errors more in Go sources than in Java ? The reverse is true !
But Go software is more stable. How can this happen ?
Well, Go defaults to ignoring errors and just continuing whereas Java (and every sane language) defaults to aborting the program rather than letting it run in an unknown state.
To put it more extreme, and more to the point Go just starts "sudo rm -Rf /" when an error happens. The reality is that Go programs just start writing things to the database based on wrong information when an error happens.
3) "As for timeouts, I am not sure why you wouldn't just pass in a context, which already has provisions for a deadline, explicit cancellation, and cancelling further library calls..."
Again, this sounds cool. You can recognize code that actually uses this correctly.
Incorrect (in more ways than one):
if _, err = f.Write(something); err != nil {
return err
}
More correct: var err error
cont := true
for retry := 0; cont; retry++ {
if retry > max_retries {
return fmt.Errorf("couldn't do X in max retries. Last error was: %v", err)
}
didit := make(chan bool)
go func() {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("the call panicked: %v", r)
}
didit <- true;
}
n := 0
for n < len(something) {
i, err = f.Write(something);
if err != nil {
return
}
n += i
}
}
select {
case <-didit:
// Ok, go to next statement
if err == nil {
cont = false
}
case <-ctx.Done():
return fmt.Errorf("context signaled abort")
case <-time.After(timeout):
}
}
Have you seen such a code style ... even once ?And what will this do ?
f.Write(something)
One of 5 things:1) it may actually work (one hopes, the common case)
2) it may not write anything
3) it may PARTIALLY write what you asked to be written (including not writing anything at all and returning EAGAIN, which is why the retry logic is not really optional. Frankly you should have a higher max_retries, and ignoring for the EAGAIN case to avoid some very rare circumstances)
4) it may panic
5a) it may block for a long time
5b) it may block forever (and even make your program unkillable. I mean, we've all used NFS, right ?)
What will be your total amount of material to diagnose this problem when it occurs in cases 1-3 ? Nothing whatsoever.
Case 4 ? A large collection of stacktraces. Thankfully usually with the relevant one on top.
5a and 5b ? 100 stacktraces, when you finally kill it (or ... well Go "supports hundreds of thousands goroutines"), one of which is relevant. Well the truth is that it may actually have so many goroutines that it ... well I managed to run out of diskspace once. But it's easily 10 megabytes and more in a real webserver.
I was going to write about how often I saw the res, _ := … error handling punt and wanted to confirm that lack of even a warning but https://play.golang.org/ really shows how deep the cognitive dissonance goes. The very first example many people are going to see doesn't even acknowledge the possibility of error handling.
That would be safe in a language which uses exceptions or something similar or in a language where the type checker forces you to do something with the return value but Go has this odd mix of learning from C and ignoring its most important lessons and treats ignoring errors as less of a problem than an unused import. Simply having an implied if-error-than-panic check any time someone assigns a value which is never checked would go a long way towards turning subtle hard-to-debug failures-at-a-distance into clear failures at the source.
Edit: none of the examples on the golang.org homepage handle errors. You appear to have to go into the fourth section of the language tour to find error handling first being discussed 19 pages into the ”Methods and interfaces” section to find error handling discussed at all and even after that point I only saw a couple of subsequent lessons where this looked like a thing which every working developer should be routinely handling (i.e. the web crawler acknowledges that fetching network resources can fail).
That would be safe in a language which uses exceptions or something similar or in a language where the type checker forces you to do something with the return value but Go has this odd mix of learning from C and ignoring its most important lessons and treats ignoring errors as less of a problem than an unused import. Simply having an implied if-error-than-panic check any time someone assigns a value which is never checked would go a long way towards turning subtle hard-to-debug failures-at-a-distance into clear failures at the source.
Edit: none of the examples on the golang.org homepage handle errors. You appear to have to go into the fourth section of the language tour to find error handling first being discussed 19 pages into the ”Methods and interfaces” section to find error handling discussed at all and even after that point I only saw a couple of subsequent lessons where this looked like a thing which every working developer should be routinely handling (i.e. the web crawler acknowledges that fetching network resources can fail).
> the res, _ := … error handling punt and wanted to confirm that lack of even a warning
Warnings don't exist in go build, because warnings are ignored by people. IIRC there are linters that do highlight those if you care for it and want hints about things to check.
There are legitimate use cases of swallowing error return values†, and if you throw warnings intros cases, then you can't tell the difference between a legit warning and a legit use case that shouldn't warn.
> none of the examples on the golang.org homepage handle error. You appear to have to go into the fourth section of the language tour to find error handling first being discussed 19 pages into the ”Methods and interfaces” section
Skimming over the fact that this is a "Tour of Go", not a full-blown tutorial, there is not a single call that would return an error before the page you mentioned. There are ok return values for map fetches and type assertions before that though, but they're just printed out, and the same pattern continues afterwards. Those are focused code snippets to show features and get the vibe of things. I certainly wouldn't expect from a tour (for any language) to be the equivalent of The Go Programming Language book, nor Effective Go[0].
[0]: https://golang.org/doc/effective_go.html#errors
† It happens that by design you may have the guarantee that e.g ParseInt will work because you're under full control of the input type and/or value.
Warnings don't exist in go build, because warnings are ignored by people. IIRC there are linters that do highlight those if you care for it and want hints about things to check.
There are legitimate use cases of swallowing error return values†, and if you throw warnings intros cases, then you can't tell the difference between a legit warning and a legit use case that shouldn't warn.
> none of the examples on the golang.org homepage handle error. You appear to have to go into the fourth section of the language tour to find error handling first being discussed 19 pages into the ”Methods and interfaces” section
Skimming over the fact that this is a "Tour of Go", not a full-blown tutorial, there is not a single call that would return an error before the page you mentioned. There are ok return values for map fetches and type assertions before that though, but they're just printed out, and the same pattern continues afterwards. Those are focused code snippets to show features and get the vibe of things. I certainly wouldn't expect from a tour (for any language) to be the equivalent of The Go Programming Language book, nor Effective Go[0].
[0]: https://golang.org/doc/effective_go.html#errors
† It happens that by design you may have the guarantee that e.g ParseInt will work because you're under full control of the input type and/or value.
> Warnings don't exist in go build, because warnings are ignored by people.
Okay, make them errors - an unused import is an error and the risk is orders of magnitude lower.
> There are legitimate use cases of swallowing error return values†, and if you throw warnings intros cases, then you can't tell the difference between a legit warning and a legit use case that shouldn't warn.
Nobody is saying there aren’t cases where you can legitimately ignore them. My position is just that it should require intent rather than being the default: you should have to access any error type at least once after assignment, even if it’s just to intentionally ignore an expected error (which is also important for confirming that the error you’re ignoring is the one you expected and not something else).
Regarding examples, yes, there’s a balance of not overcrowding examples but people retain early lessons for long periods of time. Since almost all non-trivial Go programs work with things like files and networks, it should be more prominent because the language repeats the C style approach of leaving error handling up to programmer diligence and that means you need to develop the habit from the beginning because the language doesn’t have any other protections.
Okay, make them errors - an unused import is an error and the risk is orders of magnitude lower.
> There are legitimate use cases of swallowing error return values†, and if you throw warnings intros cases, then you can't tell the difference between a legit warning and a legit use case that shouldn't warn.
Nobody is saying there aren’t cases where you can legitimately ignore them. My position is just that it should require intent rather than being the default: you should have to access any error type at least once after assignment, even if it’s just to intentionally ignore an expected error (which is also important for confirming that the error you’re ignoring is the one you expected and not something else).
Regarding examples, yes, there’s a balance of not overcrowding examples but people retain early lessons for long periods of time. Since almost all non-trivial Go programs work with things like files and networks, it should be more prominent because the language repeats the C style approach of leaving error handling up to programmer diligence and that means you need to develop the habit from the beginning because the language doesn’t have any other protections.
The lack of "solves this problem in a mature way" examples in the official tutorials / documentation is IMO a big part of why people think Go code is simpler.
Of course it's simpler if you ignore things. Or don't respect cancellation signals. But you shouldn't do that. And once you do, Go starts to get incredibly verbose and error-prone, and you have relatively weak tools for reducing that duplication.
Of course it's simpler if you ignore things. Or don't respect cancellation signals. But you shouldn't do that. And once you do, Go starts to get incredibly verbose and error-prone, and you have relatively weak tools for reducing that duplication.
You are right that go lets you shoot yourself in the foot pretty hard. Hopefully all the underscores in foo, _ := bar() makes your code reviewer think "nope". But I don't think it's better or worse than the alternative, just different. I don't find if err != nil { return err } to be that onerous, because I like consciously making the decision "this function call can fail and this is exactly what I want to happen". I feel like in the end you get the same effect as exceptions, without having an additional concept in your programming language. Tests are easy to write, make something fail and then check what err is equal to. No magic required.
I think recover() is basically almost always wrong, except around libraries that are written incorrectly and panic instead of returning errors for recoverable faults. (panic("your password is incorrect")). Writing to a closed channel or indexing off the end of an array is not a runtime error, but an error in the design of the program. I am not sure what you are saving by keeping the program alive.
So for your example, I think you are basically correct that that's what things should look like. Kill the defer/recover and let the context control the timeout. If you want to try things 3 times and have it either succeed or timeout, I don't see what choice you have but to have a for loop with 3 iterations and something that waits for the result or the timeout.
Perhaps what people want is a library that handles all failures correctly so that they don't see the possible ways things can go wrong. That's a worthy goal and I encourage people to try. In the original article, it sounded like that's what the author wanted, and his solution was to return errors out of band and have "something else" "handle them" "later". Let me know how that works out for you. If only it were that easy!
I think recover() is basically almost always wrong, except around libraries that are written incorrectly and panic instead of returning errors for recoverable faults. (panic("your password is incorrect")). Writing to a closed channel or indexing off the end of an array is not a runtime error, but an error in the design of the program. I am not sure what you are saving by keeping the program alive.
So for your example, I think you are basically correct that that's what things should look like. Kill the defer/recover and let the context control the timeout. If you want to try things 3 times and have it either succeed or timeout, I don't see what choice you have but to have a for loop with 3 iterations and something that waits for the result or the timeout.
Perhaps what people want is a library that handles all failures correctly so that they don't see the possible ways things can go wrong. That's a worthy goal and I encourage people to try. In the original article, it sounded like that's what the author wanted, and his solution was to return errors out of band and have "something else" "handle them" "later". Let me know how that works out for you. If only it were that easy!
> one of the more common error cases is nil, closely followed by out of memory and zero division ... which in Go causes a panic just like every other language.
Just FYI, out of memory is Go doesn't cause a panic. It is a fatal error which is unrecoverable and will crash the whole program.
BTW, your "More correct" version code is hilarious and very not professional.
Just FYI, out of memory is Go doesn't cause a panic. It is a fatal error which is unrecoverable and will crash the whole program.
BTW, your "More correct" version code is hilarious and very not professional.
It's mostly there to illustrate how a basic attempt to create a correct way to use a simple system call would look like, with error, retry, actually writing the entire buffer and timeout would look like.
I realize that it'd be lunacy to have this sort of code anywhere but in a library.
I realize that it'd be lunacy to have this sort of code anywhere but in a library.
> I am not sure why people rely on hacks when there's already a standard method built into the language.
IIRC, contexts weren't added to the language until fairly late. Some people + projects got used to various degrees of homebuilt ad-hoc contexts.
IIRC, contexts weren't added to the language until fairly late. Some people + projects got used to various degrees of homebuilt ad-hoc contexts.
The error handling story in Go had started to bother me more and more over the years. Not that it was that bad, it just felt cumbersome.
And then I started learning Rust for embedded development. I basically love the entire language, and there are very, very few design decisions I disagree with.
The error handling story is much better there, and if they're making breaking changes in Go 2.0, I'd suggest they look there for a better path forward.
> Panic-Driven Error Handling
I'd insert a gif of Vader at this point, from the end of Episode 3.
I strongly disagree with this, using 'panic' should be a last resort in most situations, when the program has no reasonable way to recover from an error condition. File not found, user error, etc. are all common occurrences which should be handled by the normal mechanisms. And this goes double for library code.
And then I started learning Rust for embedded development. I basically love the entire language, and there are very, very few design decisions I disagree with.
The error handling story is much better there, and if they're making breaking changes in Go 2.0, I'd suggest they look there for a better path forward.
> Panic-Driven Error Handling
I'd insert a gif of Vader at this point, from the end of Episode 3.
I strongly disagree with this, using 'panic' should be a last resort in most situations, when the program has no reasonable way to recover from an error condition. File not found, user error, etc. are all common occurrences which should be handled by the normal mechanisms. And this goes double for library code.
I mostly agree with this (especially that panic should not be used for error handling, but only for exceptional circumstances). I think checked enum style errors are the way to go, but I also think that Go's approach suffices. A popular criticism of Go's approach is that the compiler doesn't force you to handle your errors, which is true, but I find that Go's culture suffices here--the biggest complaint I have about Go's error handling is that you have to use a library to get stack traces (which is also true for Rust) and stack traces are simply very practical.
The other popular criticism of Go's error handling is that it's verbose, which I wholly disagree with--the verbosity is such a negligible cost that I would much prefer verbose error handling to the language complexity required to elide them (especially if the proposed sugar required baking "errors" into the language as opposed to treating them like any other data).
The other popular criticism of Go's error handling is that it's verbose, which I wholly disagree with--the verbosity is such a negligible cost that I would much prefer verbose error handling to the language complexity required to elide them (especially if the proposed sugar required baking "errors" into the language as opposed to treating them like any other data).
> A popular criticism of Go's approach is that the compiler doesn't force you to handle your errors, which is true, but I find that Go's culture suffices here...
The Go culture does suffice, but the Rust culture is even stronger here. Everyone is expected to use Result for something that can fail, which I like a lot for consistency's sake.
> The other popular criticism of Go's error handling is that it's verbose, which I wholly disagree with--the verbosity is such a negligible cost that I would much prefer verbose error handling to the language complexity required to elide them (especially if the proposed sugar required baking "errors" into the language as opposed to treating them like any other data).
Six months ago I would have agreed with you. And then I learned about error propagation using the question mark operator in Rust:
https://doc.rust-lang.org/book/second-edition/ch09-02-recove...
Arguments can be made about creating custom error messages at each call level, but is that really necessary most of the time?
The Go culture does suffice, but the Rust culture is even stronger here. Everyone is expected to use Result for something that can fail, which I like a lot for consistency's sake.
> The other popular criticism of Go's error handling is that it's verbose, which I wholly disagree with--the verbosity is such a negligible cost that I would much prefer verbose error handling to the language complexity required to elide them (especially if the proposed sugar required baking "errors" into the language as opposed to treating them like any other data).
Six months ago I would have agreed with you. And then I learned about error propagation using the question mark operator in Rust:
https://doc.rust-lang.org/book/second-edition/ch09-02-recove...
Arguments can be made about creating custom error messages at each call level, but is that really necessary most of the time?
"And then I learned about error propagation using the question mark operator in Rust:... Arguments can be made about creating custom error messages at each call level, but is that really necessary most of the time?"
Actually, one of the things that has prevented Go from "simply" or "just" solving the error propagation problem is precisely the recognition that in real code,
I'd suggest to a lot of people that it may be worth reading or re-reading the Go 2 error proposal [1], and to try to come at it with some relatively fresh eyes, rather than reading it for "does this precisely conform to my preconceived notions about what error handling should be?" (Another interesting aspect is around providing some base API for errors, and processing composite errors, rather than expecting end-users to do all the definition [2]. Personally I consider both important.) There is some legitimately interesting discussion and work around what to do, based on waiting to gather a lot of data about error handling before just slamming a solution into place, moreso than may meet the eye. I can't speak to Rust's solution in practice, but having used Haskell for a long time with union types for errors, I can say they are hardly error-handling nirvana there. If I'm reading the Rust documentation correctly, they're going to have the same problems in Rust as they do in Haskell.
[1]: https://go.googlesource.com/proposal/+/master/design/go2draf...
[2]: https://go.googlesource.com/proposal/+/master/design/go2draf...
Actually, one of the things that has prevented Go from "simply" or "just" solving the error propagation problem is precisely the recognition that in real code,
if err != nil {
return err
}
becomes problematic at scale, because it obscures the history of the error. Stack traces are great for humans, but not terribly useful for programmatic interpretation. Personally, I still tend to use "if err != nil { return err }" as my sort of default sketch implementation, but I find it is very common for many of those to grow something else before the program ships, and I'm still not all that great or consistent about layering errors properly anyhow.I'd suggest to a lot of people that it may be worth reading or re-reading the Go 2 error proposal [1], and to try to come at it with some relatively fresh eyes, rather than reading it for "does this precisely conform to my preconceived notions about what error handling should be?" (Another interesting aspect is around providing some base API for errors, and processing composite errors, rather than expecting end-users to do all the definition [2]. Personally I consider both important.) There is some legitimately interesting discussion and work around what to do, based on waiting to gather a lot of data about error handling before just slamming a solution into place, moreso than may meet the eye. I can't speak to Rust's solution in practice, but having used Haskell for a long time with union types for errors, I can say they are hardly error-handling nirvana there. If I'm reading the Rust documentation correctly, they're going to have the same problems in Rust as they do in Haskell.
[1]: https://go.googlesource.com/proposal/+/master/design/go2draf...
[2]: https://go.googlesource.com/proposal/+/master/design/go2draf...
> I can't speak to Rust's solution in practice, but having used Haskell for a long time with union types for errors, I can say they are hardly error-handling nirvana there.
Haskell's Either-like Monads have gone through considerable evolution:
https://www.yesodweb.com/blog/2016/04/fixing-monad-either
I'd love to hear more from someone familiar with Haskell and Rust to talk about if Rust's Result enum would have the same kinds of issues they've had in the Haskell world.
Haskell's Either-like Monads have gone through considerable evolution:
https://www.yesodweb.com/blog/2016/04/fixing-monad-either
I'd love to hear more from someone familiar with Haskell and Rust to talk about if Rust's Result enum would have the same kinds of issues they've had in the Haskell world.
It's been a while since I've actually written haskell, but a lot of this is about monads, both Monad and monad transformers, both of which Rust doesn't have.
> The broken fail function
We don't have this.
> The problem with Error
Our Error trait (typeclass in haskell terms) had a "description" method that had some issues, so we've deprecated it and provided a default impl. We expect errors to implement a similar type signature, but with the Display/Debug traits, not some sort of fromString trait.
> The proper Monad Either instance
not applicable
> Either for arbitrary error types
Rust's Result already works for arbitrary error types; you don't have to actually implement Error for them, so this is a non-issue.
> The broken fail function
We don't have this.
> The problem with Error
Our Error trait (typeclass in haskell terms) had a "description" method that had some issues, so we've deprecated it and provided a default impl. We expect errors to implement a similar type signature, but with the Display/Debug traits, not some sort of fromString trait.
> The proper Monad Either instance
not applicable
> Either for arbitrary error types
Rust's Result already works for arbitrary error types; you don't have to actually implement Error for them, so this is a non-issue.
if err != nil {
return nil, errors.Wrap(err, "bad stuff during foo")
}
looks pretty handy, I just want to tell the compiler to infer that everywhere by default, because writing it over and over is not only a waste of time but actively makes all code harder to read.That's part of what's going into the new error proposal. (Although it's not going to be at the global level, but at the function level. "At the global level" becomes tricky if you try to actually spec it out; it's easy enough to just slap some designs down, but ones that compose well aren't so easy.)
That code seems somewhat like Python's chained exceptions.
> Six months ago I would have agreed with you. And then I learned about error propagation using the question mark operator in Rust:
https://doc.rust-lang.org/book/second-edition/ch09-02-recove....
Arguments can be made about creating custom error messages at each call level, but is that really necessary most of the time?
No, this is not necessary most of the time, and I don't consider the "custom error messages" argument to be a very good argument against sugar. The convincing argument in my mind is that unlike Rust, Go has no trait system to facilitate this sugar, so the alternative is to special-case the compiler to support the error type. Of course, Go could implement a trait system, but "error handling sugar" is a negligible factor in whether or not such a system should exist and how it should be designed.
TL;DR--what makes sense for Rust may not make sense for Go.
No, this is not necessary most of the time, and I don't consider the "custom error messages" argument to be a very good argument against sugar. The convincing argument in my mind is that unlike Rust, Go has no trait system to facilitate this sugar, so the alternative is to special-case the compiler to support the error type. Of course, Go could implement a trait system, but "error handling sugar" is a negligible factor in whether or not such a system should exist and how it should be designed.
TL;DR--what makes sense for Rust may not make sense for Go.
"The convincing argument in my mind is that unlike Rust, Go has no trait system to facilitate this sugar, so the alternative is to special-case the compiler to support the error type."
What do you need out of "traits" that "interfaces" don't provide, for error handling?
What do you need out of "traits" that "interfaces" don't provide, for error handling?
There isn't infrastructure in the Go compiler for mapping a given sugar to a particular interface. The point isn't traits vs interfaces, but rather that the infrastructure exists in Rust and it doesn't exist in Go (and adding it to Go solely to alleviate a tiny bit of boilerplate is a bad idea).
> I would much prefer verbose error handling to the language complexity required to elide them
Rust's "language complexity required to elide them" is quite literally a single postfix operator, which was added as an alternative to a small macro whose behaviour had proved wildly popular.
Rust's "language complexity required to elide them" is quite literally a single postfix operator, which was added as an alternative to a small macro whose behaviour had proved wildly popular.
My comment wasn't meant as a criticism of Rust. Rust's trait system does the heavy lifting for enabling this operator (and all kinds of sugar, for that matter), so this makes sense for Rust. Go doesn't have a trait system (though it could well be a useful addition!) or a macro system, so the alternative is to treat errors as a special type instead of plain-old-data as they are today.
> Rust's trait system does the heavy lifting for enabling this operator
Are you sure? I thought that try! was a simple macro: https://doc.rust-lang.org/src/core/macros.rs.html#299
Are you sure? I thought that try! was a simple macro: https://doc.rust-lang.org/src/core/macros.rs.html#299
?[1] is indeed based on traits and try! is a macro that depends on the trait system (line 303: $crate::convert::From::from(err)) to do the very important job of propagation and coercion, by means of error-wrapping. If all it could do was pass the error forward, it'd be quite annoying to deal with. Specifically that since rust represents errors with enums, and you have to handle every error, the eventual caller function would end up dealing with 10 different possible errors, defined in 10 different libraries, and has to be made aware of all downstream dependencies. Or you'd end up avoiding the operator altogether, and force the coercion in every instance, defeating the point of the operator.
The ?/try! utilizes traits to instead allows you to auto-coerce them to a new error with a relevant message/trace (given the appropriate Into definition), allowing it to be a simple one-character addition with most of the expressive power of exceptions.
The heavy lifting is specifically that coercion; the "continue or return" part isn't that significant (that would just be avoiding the any language one-liner: if err return err)
[1] https://github.com/rust-lang/rfcs/blob/master/text/0243-trai...
The ?/try! utilizes traits to instead allows you to auto-coerce them to a new error with a relevant message/trace (given the appropriate Into definition), allowing it to be a simple one-character addition with most of the expressive power of exceptions.
The heavy lifting is specifically that coercion; the "continue or return" part isn't that significant (that would just be avoiding the any language one-liner: if err return err)
[1] https://github.com/rust-lang/rfcs/blob/master/text/0243-trai...
> The ?/try! utilizes traits to instead allows you to auto-coerce them to a new error with a relevant message/trace (given the appropriate Into definition), allowing it to be a simple one-character addition with most of the expressive power of exceptions.
That is not a very important job, because it's a context-less conversion and thus only suitable for pretty basic conversions.
> the "continue or return" part isn't that significant (that would just be avoiding the any language one-liner: if err return err)
"continue or return" is by far the most important part of ?/try!, it reduces an extensive match into a single character or 6, making error handling both terse and simple for the common case where you just want to bubble up the error.
Even if it did not do any conversion, such conversion is just a map_err away (and commonly necessary either way as the developer wants to add context or needs to convert from trait object or some such).
The automatic conversion is a minor convenience.
That is not a very important job, because it's a context-less conversion and thus only suitable for pretty basic conversions.
> the "continue or return" part isn't that significant (that would just be avoiding the any language one-liner: if err return err)
"continue or return" is by far the most important part of ?/try!, it reduces an extensive match into a single character or 6, making error handling both terse and simple for the common case where you just want to bubble up the error.
Even if it did not do any conversion, such conversion is just a map_err away (and commonly necessary either way as the developer wants to add context or needs to convert from trait object or some such).
The automatic conversion is a minor convenience.
> the alternative is to treat errors as a special type instead of plain-old-data as they are today.
They're already a special type to an extent, since Go's MRV are not first-class it could add builtin facilities applicable to any 1..n-ary return value whose last value is either an Error or an Error-implementing struct or somesuch.
They're already a special type to an extent, since Go's MRV are not first-class it could add builtin facilities applicable to any 1..n-ary return value whose last value is either an Error or an Error-implementing struct or somesuch.
> They're already a special type to an extent
This is news to me. How so?
> since Go's MRV are not first-class it could add builtin facilities applicable to any 1..n-ary return value whose last value is either an Error or an Error-implementing struct or somesuch.
MRV is "first-class" for the purposes of this discussion. Your proposal still depends on treating `error` as a special type, so my criticisms still apply--the juice just isn't worth the squeeze. That said, if another mechanism is introduced (for example, a trait-like system for hanging sugar off of a la Rust's), then the calculus changes; however, as previously mentioned, error handling sugar is not a good reason to add such a system.
This is news to me. How so?
> since Go's MRV are not first-class it could add builtin facilities applicable to any 1..n-ary return value whose last value is either an Error or an Error-implementing struct or somesuch.
MRV is "first-class" for the purposes of this discussion. Your proposal still depends on treating `error` as a special type, so my criticisms still apply--the juice just isn't worth the squeeze. That said, if another mechanism is introduced (for example, a trait-like system for hanging sugar off of a la Rust's), then the calculus changes; however, as previously mentioned, error handling sugar is not a good reason to add such a system.
> MRV is "first-class" for the purposes of this discussion.
MRV is not first-class for any purpose.
> Your proposal still depends on treating `error` as a special type, so my criticisms still apply--the juice just isn't worth the squeeze. That said, if another mechanism is introduced
There is no need for "an other mechanism" since as you yourself note the only need is for the compiler to understand the existence of error, which would require no change to userland code or break any existing.
Hell, technically and if you want a lowest denominator that isn't even necessary, you can hang it off of the MRV itself (which is very much a built-in special-purpose not-first-class construct) and have syntactic sugar for special-handling of the last return value, irrespective of its actual type.
> for example, a trait-like system for hanging sugar off of a la Rust's
try! and ? don't "hang off of" a trait, they work specifically on `Result`[0]. They only use traits for the sub-feature of automatic conversion of the error value, which IME is only a small and non-necessary part of the feature: usually you either return the error as-is or need to add context beyond what can be extracted from the original.
[0] the Try trait aims to eventually allow its use beyond these, but is currently nightly-only
MRV is not first-class for any purpose.
> Your proposal still depends on treating `error` as a special type, so my criticisms still apply--the juice just isn't worth the squeeze. That said, if another mechanism is introduced
There is no need for "an other mechanism" since as you yourself note the only need is for the compiler to understand the existence of error, which would require no change to userland code or break any existing.
Hell, technically and if you want a lowest denominator that isn't even necessary, you can hang it off of the MRV itself (which is very much a built-in special-purpose not-first-class construct) and have syntactic sugar for special-handling of the last return value, irrespective of its actual type.
> for example, a trait-like system for hanging sugar off of a la Rust's
try! and ? don't "hang off of" a trait, they work specifically on `Result`[0]. They only use traits for the sub-feature of automatic conversion of the error value, which IME is only a small and non-necessary part of the feature: usually you either return the error as-is or need to add context beyond what can be extracted from the original.
[0] the Try trait aims to eventually allow its use beyond these, but is currently nightly-only
> MRV is not first-class for any purpose.
You're going to have to elaborate because MRV as it exists today is sufficient for your proposal. If you mean "you're returning something that looks like a tuple, but you can't use it like a tuple" then that's all true but I don't see how that's necessary to implement your propsal.
> There is no need for "an other mechanism" since as you yourself note the only need is for the compiler to understand the existence of error, which would require no change to userland code or break any existing.
I agree that this is true, but I disagree that it's advisable to make `error` a special type to the compiler.
> Hell, technically and if you want a lowest denominator that isn't even necessary, you can hang it off of the MRV itself (which is very much a built-in special-purpose not-first-class construct) and have syntactic sugar for special-handling of the last return value, irrespective of its actual type.
Sure, but still a bad idea.
> try! and ? don't "hang off of" a trait, they work specifically on `Result`[0]. They only use traits for the sub-feature of automatic conversion of the error value, which IME is only a small and non-necessary part of the feature: usually you either return the error as-is or need to add context beyond what can be extracted from the original.
I was referring to the Try trait, but as you mention, it's only available on nightly. Nevertheless, I object to making the error type special to the compiler for the negligible advantage of eliding some boilerplate. If this is facilitated by some more general mechanism (e.g., monads or the Try trait), then so be it. Do note that this is my opinion, and you're free to disagree with it.
You're going to have to elaborate because MRV as it exists today is sufficient for your proposal. If you mean "you're returning something that looks like a tuple, but you can't use it like a tuple" then that's all true but I don't see how that's necessary to implement your propsal.
> There is no need for "an other mechanism" since as you yourself note the only need is for the compiler to understand the existence of error, which would require no change to userland code or break any existing.
I agree that this is true, but I disagree that it's advisable to make `error` a special type to the compiler.
> Hell, technically and if you want a lowest denominator that isn't even necessary, you can hang it off of the MRV itself (which is very much a built-in special-purpose not-first-class construct) and have syntactic sugar for special-handling of the last return value, irrespective of its actual type.
Sure, but still a bad idea.
> try! and ? don't "hang off of" a trait, they work specifically on `Result`[0]. They only use traits for the sub-feature of automatic conversion of the error value, which IME is only a small and non-necessary part of the feature: usually you either return the error as-is or need to add context beyond what can be extracted from the original.
I was referring to the Try trait, but as you mention, it's only available on nightly. Nevertheless, I object to making the error type special to the compiler for the negligible advantage of eliding some boilerplate. If this is facilitated by some more general mechanism (e.g., monads or the Try trait), then so be it. Do note that this is my opinion, and you're free to disagree with it.
> You're going to have to elaborate because MRV as it exists today is sufficient for your proposal. If you mean "you're returning something that looks like a tuple, but you can't use it like a tuple" then that's all true but I don't see how that's necessary to implement your propsal.
First-class features can be manipulated as regular values of the language.
That Go's MRV are not first-class are an advantage with respect to my proposal: they're already a "magical" built-in which can't be manipulated from within go. As a result, new features built on MRV can't conflict with existing manipulations of MRV.
> I was referring to the Try trait, but as you mention, it's only available on nightly.
And a very recent addition: `try!` does not use it and I've not seen any plan to retrofit it. Try is a way to extend ? to non-Result structures. ? is not built upon Try, Try is being extracted from ?.
First-class features can be manipulated as regular values of the language.
That Go's MRV are not first-class are an advantage with respect to my proposal: they're already a "magical" built-in which can't be manipulated from within go. As a result, new features built on MRV can't conflict with existing manipulations of MRV.
> I was referring to the Try trait, but as you mention, it's only available on nightly.
And a very recent addition: `try!` does not use it and I've not seen any plan to retrofit it. Try is a way to extend ? to non-Result structures. ? is not built upon Try, Try is being extracted from ?.
> That Go's MRV are not first-class are an advantage with respect to my proposal: they're already a "magical" built-in which can't be manipulated from within go. As a result, new features built on MRV can't conflict with existing manipulations of MRV.
That's still irrelevant to your sugar proposal; even if they were properly tuples (tuples are the proper term for "first class MRV" per your definition), the _type_ of the tuple (e.g., `(int, int, error)`) would be the thing you would base your sugar off of, not the actual value.
> And a very recent addition: `try!` does not use it and I've not seen any plan to retrofit it. Try is a way to extend ? to non-Result structures. ? is not built upon Try, Try is being extracted from ?.
Cool, but that doesn't change the calculus for Go. Hard-coding sugar to a specific type remains a bad idea.
That's still irrelevant to your sugar proposal; even if they were properly tuples (tuples are the proper term for "first class MRV" per your definition), the _type_ of the tuple (e.g., `(int, int, error)`) would be the thing you would base your sugar off of, not the actual value.
> And a very recent addition: `try!` does not use it and I've not seen any plan to retrofit it. Try is a way to extend ? to non-Result structures. ? is not built upon Try, Try is being extracted from ?.
Cool, but that doesn't change the calculus for Go. Hard-coding sugar to a specific type remains a bad idea.
FWIW, part of why Try has taken a really long time to stabilize is that there's not a ton of pressure, having it for Result was about 80% of the power, Option about 10% more.
> quite literally a single postfix operator
I disagree that the postfix operator is the primary language feature allowing rust's error handling. There is also the Result, aka a Generic Sum Type.
Go does not have generics or sum types, so it can't easily implement the generic 'Result<T, E>'. There's more to this than a bit of sugar.
Note that go does not have a generic "Into" either, which is a godsend for rust's errors.
I disagree that the postfix operator is the primary language feature allowing rust's error handling. There is also the Result, aka a Generic Sum Type.
Go does not have generics or sum types, so it can't easily implement the generic 'Result<T, E>'. There's more to this than a bit of sugar.
Note that go does not have a generic "Into" either, which is a godsend for rust's errors.
> Nothing explodes, the code keeps running
in my book, this is not a good thing but a very bad thing.
If I'm a library function somewhere all the way down the stack and I'm at a point where something about the state I'm working on isn't up to the specification I expect them to be, then I'd prefer the safety of blowing up.
I don't want to be responsible for my callers eventually ignoring my error code and happily churning along thinking that I have fulfilled my promise and enacted the side-effect I promised to have.
If something else, far removed from me depends on me to having had the side effect, it might blow up. Or it might write corrupt data. Suddenly a thing that went wrong at one place blows up at a totally different place which makes it incredibly hard to debug.
And if the world is burning, "hard to debug" is about the least wanted property a problem could have.
Oh no. If the world is burning for me, then I absolutely do not want to continue.
But I also do not want to abort the daemon process I'm being hosted in because that would mean affecting other threads of execution (be it threads, coroutines or whatever else) where the world might be in a totally acceptable state.
Calling `exit` in a library function is outright rude.
If I get to `throw`, I can make absolutely sure that I made it clear to my caller or my caller's caller that I panicked while there's still a chance for my parent daemon to not die but handle my panic cleanly. Or, my caller, or its caller was prepared for me failing and can chose another way out.
But by throwing I can raise a very strong signal that something is wrong and I have the guarantee that if I'm being ignored, then I will kill my parent which is totally their fault for ignoring me and it will be very obvious what happened, why it happened, and, above all, where it happened.
If I'm an engineer wearing my devops hat, I'd much rather debug an uncaught exception causing a stack trace to be logged in sentry.io (or whatever else you use. I'm a very happy sentry customer, but your might have your own tool) rather than corrupted data that might have been corrupted months ago.
in my book, this is not a good thing but a very bad thing.
If I'm a library function somewhere all the way down the stack and I'm at a point where something about the state I'm working on isn't up to the specification I expect them to be, then I'd prefer the safety of blowing up.
I don't want to be responsible for my callers eventually ignoring my error code and happily churning along thinking that I have fulfilled my promise and enacted the side-effect I promised to have.
If something else, far removed from me depends on me to having had the side effect, it might blow up. Or it might write corrupt data. Suddenly a thing that went wrong at one place blows up at a totally different place which makes it incredibly hard to debug.
And if the world is burning, "hard to debug" is about the least wanted property a problem could have.
Oh no. If the world is burning for me, then I absolutely do not want to continue.
But I also do not want to abort the daemon process I'm being hosted in because that would mean affecting other threads of execution (be it threads, coroutines or whatever else) where the world might be in a totally acceptable state.
Calling `exit` in a library function is outright rude.
If I get to `throw`, I can make absolutely sure that I made it clear to my caller or my caller's caller that I panicked while there's still a chance for my parent daemon to not die but handle my panic cleanly. Or, my caller, or its caller was prepared for me failing and can chose another way out.
But by throwing I can raise a very strong signal that something is wrong and I have the guarantee that if I'm being ignored, then I will kill my parent which is totally their fault for ignoring me and it will be very obvious what happened, why it happened, and, above all, where it happened.
If I'm an engineer wearing my devops hat, I'd much rather debug an uncaught exception causing a stack trace to be logged in sentry.io (or whatever else you use. I'm a very happy sentry customer, but your might have your own tool) rather than corrupted data that might have been corrupted months ago.
> in my book, this is not a good thing but a very bad thing.
Your whole reply is a straw man. The author is claiming that nothing explodes b/c the error is correctly handled, not b/c it was silently swallowed.
I've seen so many bugs caused by exceptions being silently swallowed in other threads. Exceptions are not a silver bullet here, and you end up having to pass errors as values across call stacks anyway.
Your whole reply is a straw man. The author is claiming that nothing explodes b/c the error is correctly handled, not b/c it was silently swallowed.
I've seen so many bugs caused by exceptions being silently swallowed in other threads. Exceptions are not a silver bullet here, and you end up having to pass errors as values across call stacks anyway.
"Your whole reply is a straw man. The author is claiming that nothing explodes b/c the error is correctly handled, not b/c it was silently swallowed."
Yes, but I think that this is exactly what pilif is objecting to: the fact that there is no way for the callee to force the caller to deal with the error condition (I'm not sure that this is entirely correct, given that Go has the panic/recover keywords that are later discussed in the article, but then you might as well use exceptions).
IMO, this is very similar to what happens with use-after-free and other types of memory reference errors: they are extremely hard to debug because the symptoms/results of the error can be totally disconnected from the original source of the error, in some cases by minutes or longer. You simply don't want to have a situation where improper state is allowed to linger and further pollute/corrupt any subsequent computations.
Yes, but I think that this is exactly what pilif is objecting to: the fact that there is no way for the callee to force the caller to deal with the error condition (I'm not sure that this is entirely correct, given that Go has the panic/recover keywords that are later discussed in the article, but then you might as well use exceptions).
IMO, this is very similar to what happens with use-after-free and other types of memory reference errors: they are extremely hard to debug because the symptoms/results of the error can be totally disconnected from the original source of the error, in some cases by minutes or longer. You simply don't want to have a situation where improper state is allowed to linger and further pollute/corrupt any subsequent computations.
And yet forcing the caller to deal with errors still doesn't prevent improper state in any way.
It does if they don't handle the exception, which is the situation being described.
Errors cause side effects, which is how you get to improper state. How does forcing the caller to handle errors helps with removing side effects?
Because the caller either deals with the error (and fixes the state) or they don't, but there's never a case where the callee was unable to force the caller to fix the state. If the caller then goes on to just handle/suppress the exception and continue with the invalid state, then at least they're making an explicit decision that is on them.
That's the point, forcing to handle errors doesn't force to do it properly.
It's still strictly better than just letting people not bother to handle them, which is never proper.
Is that true? What if you have an error that happens once every 10^9 requests and the service failure isn't critical (no one dies). Isn't it better to just not bother with the error, let the service keep running and don't worry about it?
That depends on whether you are going to be the person who has to find out why corrupted data has been written, when it was corrupted, what the original value was or even worse, whether a given record is corrupted or not because the corrupted ones look exactly the same as some value of non-corrupted ones.
As someone who has been in that boat, I can tell you that request termination due to an uncaught exception is infinity times better than the horror of debugging data corruption, even (or especially) when it only happens ever 10^9 requests.
As someone who has been in that boat, I can tell you that request termination due to an uncaught exception is infinity times better than the horror of debugging data corruption, even (or especially) when it only happens ever 10^9 requests.
If it's 10^9 requests, then obviously they are low-cost processes in the first place, so you're better off just re-doing the job that failed.
"perfect is the enemy of good"
Exceptions in threads is a valid objection, but designs have gotten a lot better. They now usually get automatically re-thrown whenever you access the result of futures. But that seem barely any different than return based error handling to me. If i don't access the result of some thread, I'd probably also forget to check its err.
This is why Erlang's crash early and restart philosophy was so eye opening when I became more and more familiar with the language and design. The whole language is about managing state and managing it well in a dynamic realtime environment (versus Rust's explicit state encoding for example).
Can you tell me more about Erlang's approach? Quite interested in learning of new ways to deal with state and errors.
Isolate all memory so a failure is compartamentalized
Discriminate between the error-kernel of the program and the rest. The kernel is the part which is not allowed to fail. Goal: keep it small.
Discriminate between errors you have seen happen, and everything else. Handle the errors you've seen. It looks a bit like Go's handling, but only for things where you know you have an error flow. There are also exceptions, but they are somewhat rarer in typical code.
On an unhandled error, crash the isolated process. This produces, a state of the memory, a stack trace and eventually the last few messages sent to that process.
The system has a recovery model which will restart failed processes from a last known good state.
The programming language is functional, and quite much so, so there are orders of magnitude fewer errors relating to state manipulation.
One typical approach is to use the crash as a way to learn about errors that can happen, then patch those errors by handling them. Because you have a strategy for reactively handling errors, it is far less dangerous to have them in th e program, as long as they are under a noise floor.
Discriminate between the error-kernel of the program and the rest. The kernel is the part which is not allowed to fail. Goal: keep it small.
Discriminate between errors you have seen happen, and everything else. Handle the errors you've seen. It looks a bit like Go's handling, but only for things where you know you have an error flow. There are also exceptions, but they are somewhat rarer in typical code.
On an unhandled error, crash the isolated process. This produces, a state of the memory, a stack trace and eventually the last few messages sent to that process.
The system has a recovery model which will restart failed processes from a last known good state.
The programming language is functional, and quite much so, so there are orders of magnitude fewer errors relating to state manipulation.
One typical approach is to use the crash as a way to learn about errors that can happen, then patch those errors by handling them. Because you have a strategy for reactively handling errors, it is far less dangerous to have them in th e program, as long as they are under a noise floor.
It's called the "let it crash" philosophy. The idea is that things are going to go horribly wrong no matter how well your code and everyone else's code is written. If you program for resilience, that is the ability to recover from a horribly bad event, then you can get high service availability and faster development (you're not obsessed with checking for errors, you only handle the most common ones and program to capture enough errors to achieve the service level you are comfortable with or have agreed to).
Tried to explain it here: https://news.ycombinator.com/item?id=18534441
I think this is where custom error types become useful. They can provide you more context about what went wrong so that you can handle that issue properly
Not if my caller forgets to handle my error at all
If you care, you can't realistically forget. There are linters that will find your missed error checks.
This is classic Go design, for better or worse. "Just use this ad-hoc, unprincipled tool to counter a shortcoming of the core language."
In Go, if a function returns an error you have to explicitly handle or suppress it. Otherwise code just doesn't compile.
There's no margin to "forget".
There's no margin to "forget".
It's true that functions that return (result, error) tuples are hard to miss the errors from, because you have to explicitly either at least accept it, or ignore it. It's both visually obvious that you've dropped the error and there are linters to catch this for you (which I highly recommend integrating into pre-commit hooks).
However, if a function just returns an error, but no result value, it is indeed easy to silently drop that error without realizing it by simply invoking the function and not catching the result at all. To which, again, I'd highly recommend using linters at commit time or even code save time.
However, if a function just returns an error, but no result value, it is indeed easy to silently drop that error without realizing it by simply invoking the function and not catching the result at all. To which, again, I'd highly recommend using linters at commit time or even code save time.
> In Go, if a function returns an error you have to explicitly handle or suppress it. Otherwise code just doesn't compile.
ah, yeah, great idea. in practice this just means that most Go projects are minefields of
ah, yeah, great idea. in practice this just means that most Go projects are minefields of
if err != nil { panic(err) }
or if err != nil { return err }
ie a manual & repetitive implementation of assert for the first case and manual & repetitive implementation of an exception call stack for the second. thank you very much, I'll take the automated version known as exceptions instead.Yes, Go requires explicit error propagation, but this is desirable because 1) errors are no different than other values so they don’t get special flow control and 2) people are much more likely to properly handle errors if they have to think about handling in every call frame (I.e., error handling bugs are much rarer in Go than Python or JS or etc).
>>> (I.e., error handling bugs are much rarer in Go than Python or JS or etc).
[citation needed]
[citation needed]
I've been writing Python extensively for 10 years and Go for 6. Recently, I've been the exceptions cop in our Python/JS shop, and every week I see errors in prod that would almost certainly not exist in Go code (both because Go is statically typed but also because Go developers check their errors).
Take this anecdote for whatever it's worth to you.
Take this anecdote for whatever it's worth to you.
This isn’t true. This is a valid program, after all—even though fmt.Println returns (int, error):
func main() {
fmt.Println(“Hello”)
}> In Go, if a function returns an error you have to explicitly handle or suppress it.
If a function only returns an error, or if it returns both a value and an error and you care for neither, you can absolutely ignore them entirely.
And when you can "suppress" an error and still access the value, it's not really impressive.
If a function only returns an error, or if it returns both a value and an error and you care for neither, you can absolutely ignore them entirely.
And when you can "suppress" an error and still access the value, it's not really impressive.
That's not quite true. For instance, the error value returned from `fmt.Printf`[0] is usually ignored silently.
[0] https://godoc.org/fmt#Printf
[0] https://godoc.org/fmt#Printf
file, _ := ioutil.ReadFile(filename)
Properly handling things is about reverting all side effects to get to initial predictable state from where you can start over. Context doesn't help here. What helps is isolation or rather structuring your program around lightweight isolated processes. Where you can be sure that anything within process boundaries doesn't affect any other process and so you can just restart it if something blows up in it.
Man! I actually like half of Golang's error handling practice. Returning errors and dealing with them in a simple case sounds nice, there is no magic required, no need to think "am I forgetting to catch an exception here?"
On the other hand, the fact that we are returning a tuple of (result, err) where by convention err should be nil if everything worked screams as a terrible design to me.
But I might be influenced by reading too much Haskell/ML inspired languages :D
Once you understand applicative validation [1] or exhaustive pattern matching on row-polymorphic sum types [2], other error handling methods just seem so crude :)
[1] https://leanpub.com/purescript/read#leanpub-auto-applicative... [2] http://keleshev.com/composable-error-handling-in-ocaml
On the other hand, the fact that we are returning a tuple of (result, err) where by convention err should be nil if everything worked screams as a terrible design to me.
But I might be influenced by reading too much Haskell/ML inspired languages :D
Once you understand applicative validation [1] or exhaustive pattern matching on row-polymorphic sum types [2], other error handling methods just seem so crude :)
[1] https://leanpub.com/purescript/read#leanpub-auto-applicative... [2] http://keleshev.com/composable-error-handling-in-ocaml
Who said (result, err) should be a sum type? Why must the result be invalid when err != nil?
As an aside, there is a notable Go API that a lot of people get wrong: io.Reader's Read() and io.Writer's Write() both return a count when they return io.EOF, and if the count is a positive number, the buffer was filled.
If you do a code search for Go projects on Github, a disturbing number of them get this utterly wrong, and simply do something like this:
The other problem with Go's design is that you have to read the documentation to understand what the semantics of a particular function are. The type system can't tell you.
If you do a code search for Go projects on Github, a disturbing number of them get this utterly wrong, and simply do something like this:
for {
c, err := r.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return err
}
...
When they should be doing something like: for {
c, err := r.Read(buf)
if err == io.EOF {
if c == 0 {
break
}
} else if err != nil {
return err
}
This is an unfortunate API design, because almost all error-returning functions have disjoint return values, and would could be expressed as sum types in other languages, which teaches developers to not expect both return values to be valid at the same time. It certainly surprised me.The other problem with Go's design is that you have to read the documentation to understand what the semantics of a particular function are. The type system can't tell you.
It doesn't. You can use an inclusive sum type (i.e. inclusive or), which would be success, error, or result with accompanying error. You can think of it as success, fatal error, or non-fatal error.
The same machinery still works and it's pretty straightforward to convert between the two representations (so you can track which parts of your code bail out at the first error and which parts try to soldier on).
See for example http://hackage.haskell.org/package/these or https://typelevel.org/cats/datatypes/ior.html or https://github.com/purescript-contrib/purescript-these
For a lot of FP languages that take this approach to errors you'll have three representations (bail out at first error, continue on nonfatal errors, and bail on any error but first try to accumulate as many errors as possible as a batch) that all have conversions between them depending on what you want the semantics of that section of your code to be.
The same machinery still works and it's pretty straightforward to convert between the two representations (so you can track which parts of your code bail out at the first error and which parts try to soldier on).
See for example http://hackage.haskell.org/package/these or https://typelevel.org/cats/datatypes/ior.html or https://github.com/purescript-contrib/purescript-these
For a lot of FP languages that take this approach to errors you'll have three representations (bail out at first error, continue on nonfatal errors, and bail on any error but first try to accumulate as many errors as possible as a batch) that all have conversions between them depending on what you want the semantics of that section of your code to be.
btw haskell also has a concept of exception, so it's not like that you need These or Either for all cases, in some cases using a exception might be good enough.
Exceptions do exist but their usage in Haskell is mainly restricted to code living in IO (as a result of both community customs and the restriction of handlers to IO code).
FWIW personally I think of exceptions in Haskell almost exclusively as a tool for thread management and tend to shy away from using it for only error handling (although the two do have some overlap). But that's a more divisive topic in the Haskell community at large than the ban on exceptions in pure code.
FWIW personally I think of exceptions in Haskell almost exclusively as a tool for thread management and tend to shy away from using it for only error handling (although the two do have some overlap). But that's a more divisive topic in the Haskell community at large than the ban on exceptions in pure code.
I said that I prefer it :D
And even in ML-style language, I wouldn't like a result of
(Maybe result, Maybe error)
and I would much prefer
Result result | Partial (result error) | Error error | Nothing
These two things are the same. I still like the spelled-out version better. Especially because this gives me easier way to model different things.
Consider changing my example to
Result result | Partial (result error) | Error error
or
Result result | Error error
Semantic of every one of these is different. And nicely explicit :)
And even in ML-style language, I wouldn't like a result of
(Maybe result, Maybe error)
and I would much prefer
Result result | Partial (result error) | Error error | Nothing
These two things are the same. I still like the spelled-out version better. Especially because this gives me easier way to model different things.
Consider changing my example to
Result result | Partial (result error) | Error error
or
Result result | Error error
Semantic of every one of these is different. And nicely explicit :)
It doesn't have to be, but it can be, and preventing it from being a sum type because you might sometimes want a product type seems overly complex to me.
Why wonder "do I need to catch an exception here?"? If you're doing it right, most of the time, you want to let exceptions propagate.
Hm, maybe it is the explicit nature I am after? For example I really liked the checked exceptions in Java, even though I understand the reservations of other people.
>> Relying on documentation is not a silver bullet either: poor documentation is still very much a thing.
>> It makes sense always to check the documentation to find out whether an error type is a part of the function signature.
O_o. So on the one hand you don't know if a function will throw an exception and can't rely on the docs, while on the other you don't know if it will return an error code so you should check the docs.
I don't write Go yet, although I look at a fair bit of it. A friend of mine who works a lot in it tells me he was put off at first but basically decided to trust the language's idioms for now. Until I have more personal experience my outlook is: "man, I think I would really miss exception handling" with a smattering of "boy that sure looks more error prone to me."
>> It makes sense always to check the documentation to find out whether an error type is a part of the function signature.
O_o. So on the one hand you don't know if a function will throw an exception and can't rely on the docs, while on the other you don't know if it will return an error code so you should check the docs.
I don't write Go yet, although I look at a fair bit of it. A friend of mine who works a lot in it tells me he was put off at first but basically decided to trust the language's idioms for now. Until I have more personal experience my outlook is: "man, I think I would really miss exception handling" with a smattering of "boy that sure looks more error prone to me."
Gah, my eyes! Why is the font so huge? And why doesn't zoom change it? I have to like step 5' back from my screen to read it....
Otherwise...
I have been writing Go for about 6 years... errors make so much sense to me. Why have some external codepath for "file not found"? Why is that different than "name == bob"? They're just data that is in one state or another. You check for them the same way, with an if statement. There's nothing magical.
Otherwise...
I have been writing Go for about 6 years... errors make so much sense to me. Why have some external codepath for "file not found"? Why is that different than "name == bob"? They're just data that is in one state or another. You check for them the same way, with an if statement. There's nothing magical.
If a function can't succeed there's no reason to continue executing it. Stopping and reporting the error to your caller is the common case and should not be discouraged by adding friction and obscuring the useful parts.
And yet no one is deterred from proper error handling in Go. On the other hand, I see people fail to properly handle errors all the time in our Python/JS shop.
EDIT: This isn't some language partisanship; it's my observations from years of experience with the aforementioned languages (I work in a Python/JS shop). I'm guessing there are no formal data about this, so I'd be curious to hear other anecdotes whether they agree or conflict with mine.
EDIT: This isn't some language partisanship; it's my observations from years of experience with the aforementioned languages (I work in a Python/JS shop). I'm guessing there are no formal data about this, so I'd be curious to hear other anecdotes whether they agree or conflict with mine.
Yeah, I'm not sure why you're being downvoted. We also use Python at work and reviewing code is an absolute nightmare because you end up needing to dive deep into each function call to see if there are any exceptions being ignored. We're trying to enforce adding all possible exceptions in the docstrings but it's difficult because third party libraries (and even the standard library!) sometimes don't document them. Typically we have a catch-all Exception in our main loop and recover from there in case there's something we missed in dev and staging. I'd much prefer being able to know which functions have the ability to fail and why.
Thinking about control flow and organizing for clean structure is crucial for developping code bases of non-trivial size, and that should not be discouraged by encouraging superficial efficiencies.
> And why doesn't zoom change it?
It's using `vw` font measurements (instead of `px`/`rem`).
https://css-tricks.com/viewport-sized-typography/
It's using `vw` font measurements (instead of `px`/`rem`).
https://css-tricks.com/viewport-sized-typography/
I think fancy custom ligatures are actively un-helpful when trying to demonstrate something about code to a wide audience. Is it a custom operator? Is it a fancy codepoint like APL or Julia? Is it a pre-processor macro?
Personally, I don't like them in my editor either, but if you want to use one that's fine. But they're pretty confusing when encountering in code online.
Personally, I don't like them in my editor either, but if you want to use one that's fine. But they're pretty confusing when encountering in code online.
100% ack. If I wouldn't have known ligatures, I would be totally confused about the syntax in this code examples. Am I expected to type this character? Where is this thing on my keyboard, etc.
Looking at that pile of vomit code and claiming to have reached some kind of nirvana is just ridiculous. That article is an argument against go's error handling if I ever saw one. One shouldn't have to go to that much effort just to make the handling of errors in Go, sane. To come up with that monstrosity one one hand while calling try ... catch "verbose" on the other hand is hypocrisy of the highest order.
Even go authors themselves know the current error handling is not ideal, that's why they want to change it in go 2...
see
https://go.googlesource.com/proposal/+/master/design/go2draf...
see
https://go.googlesource.com/proposal/+/master/design/go2draf...
He talks about this in the article.
So how is this better, cleaner syntax than checked exceptions in Java, for example? I thought checked exceptions were considered a failed experiment. The convention in go is to always handle the errors so isn't that the same thing?
Is it the lack of forced stacktrace that makes flow control through exception more palatable?
Do people just like convention over compiler enforced correctness?
Or is my understanding incorrect and devs don't really like go's error handling?
Is it the lack of forced stacktrace that makes flow control through exception more palatable?
Do people just like convention over compiler enforced correctness?
Or is my understanding incorrect and devs don't really like go's error handling?
There's a difference between "errors should alter the flow of control" and "error types should be transitively declared". The latter failed in Java because the language didn't have enough support for handling the layering violation. E.g., if the X ctor throws suddenly you can't implement Supplier<X> without dirty hacks like writing an explicit handler that does nothing but rethrow an unchecked type.
Throwing in constructors is an anti-pattern imo but assuming you need it, how does go solve this? Would you not return nil and an error that you would immediately check? Seems like defensively checking return values in go isn't that much better than catching every exception.
Maybe I don't understand the correct go implementation.
Maybe I don't understand the correct go implementation.
Go's error handling has flaws, but one thing it got right is having a single error type that everyone uses. Most of the time you don't think about what subtype it is.
In Java, declaring a method to throw any Exception (or should it be Throwable?) is considered an antipattern and most people use subtypes. I think if the exception hierarchy were simpler so that most of the time, there are only two kinds of methods (those that always succeed and those that can fail), checked exceptions would have worked out.
In Java, declaring a method to throw any Exception (or should it be Throwable?) is considered an antipattern and most people use subtypes. I think if the exception hierarchy were simpler so that most of the time, there are only two kinds of methods (those that always succeed and those that can fail), checked exceptions would have worked out.
This doesn't seem like a significant difference to me. You can catch all throwables and handle them the same way. Ironically, it feels worse to ignore the added exception type information so it's rare to see errors handled generically this way.
Well, that's what I mean. You could sorta do it in Java, but people don't do it that way because there aren't consistent conventions and it feels wrong. But adding the extra type information exposes internal details and results in a backward-incompatible API change if you ever want to start throwing a new exception.
Some people extend RuntimeException instead, which means you don't know whether a generic RuntimeException is recoverable or not.
It's similar to how code formatting tools have been around a long time, but Go made their formatter standard, and that made all the difference. Conventions sometimes matter more than language features.
Some people extend RuntimeException instead, which means you don't know whether a generic RuntimeException is recoverable or not.
It's similar to how code formatting tools have been around a long time, but Go made their formatter standard, and that made all the difference. Conventions sometimes matter more than language features.
It’s actually kind of amazing that programming languages basically make it as hard as possible to figure out what’s going on when you have the temerity to check for errors, and it is easier to read code that doesn’t care. Programming books love to exclude error handling “for brevity”, when the real problem is that the mechanisms for doing so are insane to begin with.
Conditionals are frustrating because they tend to be paired with indentation, which doesn’t "diff" well in a lot of tools and makes simple changes seem more extensive than they really are. In a sense, I shouldn’t have to re-indent half a function just because I happen to be changing the “preferred/happy path” that is buried inside several error checks.
On the other hand, early returns are frustrating because they enable lazy programmers to make quick hacks at the expense of complicating later maintenance (e.g. instead of having one “normal” path to consider, the next programmer has to find and understand all the short-cuts that someone has introduced throughout the code and make sure they will all work).
I suppose the most “compatible” change to existing languages would be something like a keyword or syntax to identify code that is only handling errors. That way, at least IDEs/editors/etc. could offer ways to intelligently hide code that apparently isn’t part of the normal flow of a function.
Fortunately languages are now more likely to have good mechanisms for declaring local blocks of reusable code (not banished to functions on far-away lines) so it is now more practical to write code in logical chunks that aren’t quite as indented. You can write a sequence of operations, maintain it without ugly "diffs", and still have indentation and error-checking, etc. at point of use.
Conditionals are frustrating because they tend to be paired with indentation, which doesn’t "diff" well in a lot of tools and makes simple changes seem more extensive than they really are. In a sense, I shouldn’t have to re-indent half a function just because I happen to be changing the “preferred/happy path” that is buried inside several error checks.
On the other hand, early returns are frustrating because they enable lazy programmers to make quick hacks at the expense of complicating later maintenance (e.g. instead of having one “normal” path to consider, the next programmer has to find and understand all the short-cuts that someone has introduced throughout the code and make sure they will all work).
I suppose the most “compatible” change to existing languages would be something like a keyword or syntax to identify code that is only handling errors. That way, at least IDEs/editors/etc. could offer ways to intelligently hide code that apparently isn’t part of the normal flow of a function.
Fortunately languages are now more likely to have good mechanisms for declaring local blocks of reusable code (not banished to functions on far-away lines) so it is now more practical to write code in logical chunks that aren’t quite as indented. You can write a sequence of operations, maintain it without ugly "diffs", and still have indentation and error-checking, etc. at point of use.
The combination of panic and recover is exactly like raise/throw and catch. One could even reasonably argue that both are both just a single specialized call/cc (You pass call/cc a function to execute, which receives an argument that allows immediate exit to the enclosing scope.) It's basically a kind of control flow operator. So what's the novelty here?
For some time now, I’ve been thinking that it’d be funny to add a Lisp-style condition system to Go by abusing panic, defer & recover. I think that it’d be possible (since Lisp’s conditions are really just built atop THROW & CATCH).
I don’t really think it’s a good idea: Go’s lack of macros imply rather a lot of anonymous functions to get it to work, which would imply loads of parentheses & curly brackets. Not to mention that the lack of first-class type literals would make actually using it close to insane. But it’d certainly be interesting. It’d definitely not be idiomatic Go. Still … interesting.
I don’t really think it’s a good idea: Go’s lack of macros imply rather a lot of anonymous functions to get it to work, which would imply loads of parentheses & curly brackets. Not to mention that the lack of first-class type literals would make actually using it close to insane. But it’d certainly be interesting. It’d definitely not be idiomatic Go. Still … interesting.
> For some time now, I’ve been thinking that it’d be funny to add a Lisp-style condition system to Go by abusing panic, defer & recover. I think that it’d be possible (since Lisp’s conditions are really just built atop THROW & CATCH).
How would you resume execution at the panic point?
A conditions system requires that you don't unwind, afaik Go's panic do in fact unwind.
How would you resume execution at the panic point?
A conditions system requires that you don't unwind, afaik Go's panic do in fact unwind.
> How would you resume execution at the panic point?
You invert it: rather than panicking, then continuing from the panic point if things recover, you proceed, only panicking to perform a transfer of control. Interestingly, this is what Lisp does: SIGNAL[0] (the most primitive function) just looks for applicable handlers and calls them from most- to least-recent; this means that if a handler transfers control (panics, in Go terms) then the condition has been handled; if not, SIGNAL just returns NIL; ERROR[1] does the same thing, but calls INVOKE-DEBUGGER if no handler transfers control; CERROR[2] is like ERROR, but it adds a CONTINUE restart, which just lets control resume.
It’s possible to implement this same structure in Go: conditions.Signal() would search for applicable handlers and execute them in order; if control transfers then it’d never return; conditions.Error() would invoke one or more handlers, or the debugger, and would never return; conditions.ContinuableError() would invoke one or more handlers, or the debugger, but might return.
The key is that it’s possible to execute a restart in the dynamic context of the signalling function (via use of Go’s context.Context), and only rewind the stack when you don’t need to go back down it. I think — I’ve not yet implemented it!
0: http://www.lispworks.com/documentation/lw71/CLHS/Body/f_sign...
1: http://www.lispworks.com/documentation/lw71/CLHS/Body/f_erro...
2: http://www.lispworks.com/documentation/lw71/CLHS/Body/f_cerr...
You invert it: rather than panicking, then continuing from the panic point if things recover, you proceed, only panicking to perform a transfer of control. Interestingly, this is what Lisp does: SIGNAL[0] (the most primitive function) just looks for applicable handlers and calls them from most- to least-recent; this means that if a handler transfers control (panics, in Go terms) then the condition has been handled; if not, SIGNAL just returns NIL; ERROR[1] does the same thing, but calls INVOKE-DEBUGGER if no handler transfers control; CERROR[2] is like ERROR, but it adds a CONTINUE restart, which just lets control resume.
It’s possible to implement this same structure in Go: conditions.Signal() would search for applicable handlers and execute them in order; if control transfers then it’d never return; conditions.Error() would invoke one or more handlers, or the debugger, and would never return; conditions.ContinuableError() would invoke one or more handlers, or the debugger, but might return.
The key is that it’s possible to execute a restart in the dynamic context of the signalling function (via use of Go’s context.Context), and only rewind the stack when you don’t need to go back down it. I think — I’ve not yet implemented it!
0: http://www.lispworks.com/documentation/lw71/CLHS/Body/f_sign...
1: http://www.lispworks.com/documentation/lw71/CLHS/Body/f_erro...
2: http://www.lispworks.com/documentation/lw71/CLHS/Body/f_cerr...
I see, you'd only be using panics for the unwinding handler, not for the actual conditions system.
Lisp-style conditions are implementable even over C setjmp and longjmp style control.
The non-unwinding part is done by searching the chain of frames and calling functions, without taking any non-local control transfer.
The frame linkage is assisted by maintaining a global or thread-specific stack top variable. A frame is declared on the stack, then hooked into the list using the global variable. When a control transfer does take place, the value of that variable is properly maintained to discard the frames that are in functions being abandoned.
Source: been there, implemented that.
I'm not that familiar with go but from what I remember of the descriptions of panic/recover, something similar should be doable.
The non-unwinding part is done by searching the chain of frames and calling functions, without taking any non-local control transfer.
The frame linkage is assisted by maintaining a global or thread-specific stack top variable. A frame is declared on the stack, then hooked into the list using the global variable. When a control transfer does take place, the value of that variable is properly maintained to discard the frames that are in functions being abandoned.
Source: been there, implemented that.
I'm not that familiar with go but from what I remember of the descriptions of panic/recover, something similar should be doable.
What's up with the "equal sign with a slash" being used in place of the literal "!="? I realize it's reference, but why abstract the code away like that? Seems unnecessary, and potentially confusing for new programmers.
[deleted]
Very annoying and frustrating that you cannot zoom in/out to adjust the font size in this @%$#!& site. Why take that freedom from your users?
I see a waaaay big font on my monitor (3440x1440).
I see a waaaay big font on my monitor (3440x1440).
When learning Go, I actually liked how it dealt with errors. I like that you need to aknowledge that an error could happen, but I also like that it's a normal return value.
It's not valid for _everything_ so I don't agree with it entirely. But, some things that are exceptions in Java would make more sense as just returns because it is _valid_ output for a function.
I think it's the distinction between "the method can't perform what it's supposed to perform due to the input it received" vs "something unexpected happened and that's why the method couldn't perform what it was supposed to".
I'll try to give an example:
Imagine you have a method in Java for parsing a credit card, which follows a certain pattern for verifying CSV. If the pattern wasn't satisfied, you could have a:
Whereas, for example, if you have a method that writes to a File and the file you provided does not have the right priviledge -> this is an Exception because the user could not expect this.
I'm having a hard time wording this as consisely as possible, I'm sorry if it seems a bit incomprehensive but I'll try to TL;DR:
Exceptions: Unexpected behaviour from system Error returns: Wrong input from user
If that makes sense :)
It's not valid for _everything_ so I don't agree with it entirely. But, some things that are exceptions in Java would make more sense as just returns because it is _valid_ output for a function.
I think it's the distinction between "the method can't perform what it's supposed to perform due to the input it received" vs "something unexpected happened and that's why the method couldn't perform what it was supposed to".
I'll try to give an example:
Imagine you have a method in Java for parsing a credit card, which follows a certain pattern for verifying CSV. If the pattern wasn't satisfied, you could have a:
Throw new IllegalArgumentException("Input does not match CSV string ([0-9])")
But to me _exception_ would signify something unexpected happened. Whereas here I'd like if I could just 'return' that the input was not correct, and therefore the method could not check the CSV.Whereas, for example, if you have a method that writes to a File and the file you provided does not have the right priviledge -> this is an Exception because the user could not expect this.
I'm having a hard time wording this as consisely as possible, I'm sorry if it seems a bit incomprehensive but I'll try to TL;DR:
Exceptions: Unexpected behaviour from system Error returns: Wrong input from user
If that makes sense :)
> If that makes sense :)
You're just trying to justify/rationalise that you like go's arbitrary positioning on the result/fault continuum (well not continuum, it's not really 1-dimensional), there are few positions which can't be justified there.
> But to me _exception_ would signify something unexpected happened.
Being provided random garbage when prompted for a CVV can reasonably be unexpected.
> Whereas, for example, if you have a method that writes to a File and the file you provided does not have the right priviledge -> this is an Exception because the user could not expect this.
Why would the user not expect ACL issues on something well known for involving ACLs?
You're just trying to justify/rationalise that you like go's arbitrary positioning on the result/fault continuum (well not continuum, it's not really 1-dimensional), there are few positions which can't be justified there.
> But to me _exception_ would signify something unexpected happened.
Being provided random garbage when prompted for a CVV can reasonably be unexpected.
> Whereas, for example, if you have a method that writes to a File and the file you provided does not have the right priviledge -> this is an Exception because the user could not expect this.
Why would the user not expect ACL issues on something well known for involving ACLs?
Isn't it weird that there's no language which nailed error handling and patterns 100% and which people can point to confidently and say "Do what they did - it's bulletproof"
That language is called Erlang, people always point it out, nobody listens though.
Reminds me of result types and ROP in functional languages, especially F#. Except in F# callers must handle all conditions. Maybe they can add that check to golint.
There are other ways to handle errors besides Go's overly pedantic way and exceptions. Either/Result types are in a few languages and are wonderful.
I would not ever even think about reusing any package that used the panic/recover mechanism for error handling. It's that bad.
Go's error handling is exactly the equivalent of only having checked exceptions. I've gone the other way, personally, the last few years: I prefer languages that only have unchecked exceptions.
If you call methods that throw IOException, you can just declare IOException, you don't have to litter every statement of every method with catch clauses that hurt readability.
Edit: where this breaks down in Java is trying to implement interfaces that don't declare the exceptions you're then forced to smuggle out.
Edit: where this breaks down in Java is trying to implement interfaces that don't declare the exceptions you're then forced to smuggle out.
diebir(1)
The article is useful, detailing different approaches to... deal with the ugliness; but it still lost me because of the ridiculous implication that Go's error handling is in any way right or even better than the try catch flow. It isn't, it so much isn't that even its designers are tacitly considering they're wrong with check and handle (Go's own try catch, which the OP himself mentioned).
Go is not perfect, no language is, and one of its biggest flaws is its odious error handling. And that is good, because nothing can be perfect.
Apropos of nothing, it's pretty amusing that the go faq entry criticizing exceptions has a typo.
Go is not perfect, no language is, and one of its biggest flaws is its odious error handling. And that is good, because nothing can be perfect.
Apropos of nothing, it's pretty amusing that the go faq entry criticizing exceptions has a typo.
The approach suggested for Go 2 under bargaining is basically a reverse try-catch at best and a Visual Basic “on error goto” at worst.
Even in their own attempt to explain why try catch is bad they effectively admit it serves a common use-case: A common use-case not handled by Go no less.
This lacking also puts the burden on the programmer which will have to manually handle each and every error, by himself, everywhere, one by one.
Even worse are the priorities given in the language: checking for errors is cumbersome, while ignoring errors is easy.
I’m willing to be proven wrong, but my immediate guess is that this in general leads to more buggy code, not less.