this falls pretty squarely in the category of “technically true but missing the point”.
Is Go’s panic/recover mechanism fundamentally exception-like in nature? Yes, it is. Are there codepaths that exist in which panic/recover is used for error handling? Yes, there are.
Those cases are few and far between, they’re not the dominant and main mechanism for error handling. The stdlib and the ecosystem uses the error interface and multiple return for error-handling in the vast majority of error-handling codepaths, to such a degree that it’s conventional wisdom that a package should not panic and expect that a caller call recover().
It would be strictly more accurate to say “in Go, errors are nearly always signalled through values that use the error type, which is an interface, which is a boxed fat pointer on the heap, and represents a node in a tree of error values that is traversable using the functionality provided by the errors package, except in a few situations where they use panic and recover, but generally speaking it’s unlikely you’ll ever encounter a library that calls panic and expects you to recover”, but that’s a lot of words, so we just say “Go doesn’t use exceptions for error handling”, because for nearly every person in nearly every situation that’s the useful level of accuracy that people need when they are learning the language and not fretting about every corner case up front.
Go and the ecosystem usually don’t focus on correctness, and are fine with 90% solutions. So the fact that there’s a corner case (bar() panics) wouldn’t be a concern.
You’ll just get a crash and a new K8s pod, so don’t worry about it ;)
I think ordinary Go code doesn’t run in critical contexts, so that attitude is mostly fine in practice. You just don’t want to use Go outside its target/niche.
From a design perspective, Rob Pike infamously (basically) said Googlers are not to trust with anything advanced like generics or iterators; that’s for researchers.
And simplicity is the Go ethos, even at the cost of trading off correctness in some cases.
I personally find that mentality condescending and sad as a programmer that likes to perfect their craft, and robust code, even if just to explicitly acknowledge something’s not handled.
That being said Go is not all bad, my favorite things about it are cooperative cancellation via Context, and how tooling just works*.
I do have a lot of critical opinions about it because I’ve used it a lot and my values don’t align.
Yes recover() is sprinkled at appropriate boundaries, such as request handling for net/http, and that’s definitely a good thing.
There’s also cases where you can’t recover(): some runtime behavior is a “fatal error”, not a panic. For instance unlocking an unlocked mutex, or concurrent writes to a map.
I’m not against panic at all, it makes sense in most languages, and definitely in Go’s target use cases.
That doesn’t conflict with my opinion that “usually don’t focus on correctness” is a fair assessment. The rest of my comment is maybe a bit too snarky because I was feeling jokey.
So I guess that means the problem is that the code doesn’t set x.a back to what it was originally, as though in a database transaction, in the case that bar() panics?
If bar() panics, in 99% of cases we should be letting the program crash so partial update of an object doesn’t really matter anyway. Go is being pragmatic here.
This was my thought as well. Correct me if am wrong but shouldn’t we be aware enough to handle this case when we implement it like that? Or the requirement is that the compiler should tell it?
“Go doesn’t use exceptions for error handling” is a lie, regardless of how convenient of a lie it is. A newcomer to the language is building a knowledge foundation that is wrong if presented with such statement.
Just say that there are exceptions but should almost never be used because reasons X Y Z.
If the goal is just to reduce useless Pull Request by pedant, I’d suggest using language like “C++, D, and Go use control flow mechanism like throw/catch or panic/recover, which can prevent bar() from being called”. I believe it would get the message across without triggering tedious semantic argument about what is the one true definition of “Exception”.
Its easy to have conflicting opinion on whether or not panic/recover is about “exception”, and if it is or not a good practice to use it, but it’s a hard fact that it exists in the language and that it is control flow.
Agreed. It seems like the motivation of the Zig documentation is to show that Zig has no hidden control flow - which is great! Maybe it’s better to avoid comparisons to other languages in that respect. It’s not a zero-sum game, there’s no need for value judgements.
it’s not the default method of handling errors but it does get used as one,
Hard disagree.
When I see panic -> recover used explicitly as an error mechanism in a module I stay the hell away from it. A little more acceptable is having a recover when executing code that you don’t own, like go-chi’s Recoverer middleware, but even that I only use in development environments.
EDIT: this was unnecessary snarky, sorry! What I actually wanted to say is that panic in the parser cited by kristoff is kinda an odd thing, but having a catch-all recovery around an HTTP handler is a central example, in any ecosystem, of why something similar to unannotated unwinding is necessarily useful.
There’s a long tradition of bailing out of recursive descent parsers using longjmp or exceptions. Doesn’t strike me as odd that the author chose touse the same design pattern when writing a parser in Go.
Erm, I feel like this serves an almost identical use as go-chi’s recoverer, which I already mentioned as an exception. Perhaps it would have made a better example.
I thought I was clear that what I meant was a module using panic and recover explicitly for some internal error handling.
The “development environments” part threw me off then. It seems that than this is mostly a terminology question: can the thing that the go’s net/http does around user-supplied handler be called “error handling”, or is it something else? (I personally would really love for this to have a separate word, but in the absence of one, I’d use “error handling” to describe what go does there, although that is clearly a different error handling from, eg, parsing a decimal number when the input is "❤️")
I think of it as an “error boundary”. I think I got the term from React. It’s an attempt to isolate the crash to a specific request / thread / user / region of a page.
It definitely is error handling, of the funkiest kind because it focuses on otherwise “unhandled” errors and turns them into handled ones, which works up until it doesn’t, since sometimes the unhandled-ness of the original error can’t be fully contained in the blast radius under the recover.
The talk section I linked in my other comment shows in my opinion a very compelling example where this is exactly what you see (trying to handle unhandled errors, and Go failing at that), and it’s not that hard to imagine even non-google-scale code eventually running into similar problems when dealing with any form of data that lives past the http request-response cycle (e.g. caching).
When I see panic -> recover used explicitly as an error mechanism in a module I stay the hell away from it.
Including the standard library? Because running Go in an environment where panic=abort will introduce you to all the parts of Go’s stdlib where panic/recover is used in place of return err.
To followup on this, I also disagree with the main thesis of the article: Panic/Recover is NOT exception handling because exception handling (at least to me) implies the existence of a typed recovery mechanism, which Go doesn’t have.
That’s one way of looking at it, for sure. To me a much more defining characteristic is the fact that you have to be worried of leaving corrupted state around because a function panicked and then somebody up the stack recovered leaving some fields unset, which is a trait shared by both try/catch and panic/recover.
FYI, in C++, where exceptions are the norm and not the exception (at least in some codebases; pun intended) this property is called “exception safety”: leaving objects in a valid state in the face of exception-caused stack unwinding. It takes some time and effort to get a hang of writing exception-safe C++ code. And I agree with you, allowing exception-like unwinding without a concern for state is asking for trouble.
It is typed tho, you just do it yourself. It definitely works and you should definitely check the types that you recover so you can reraise ones that shouldn’t be caught (like runtime errors).
Some people get very worked up about how Go “doesn’t have exceptions.” First of all, this is untrue. Go has panic, which works almost exactly like an exception.
If Zig code doesn’t look like it’s jumping away to call a function, then it isn’t. This means you can be sure that the following code calls only foo() and then bar(), and this is guaranteed without needing to know the types of anything
What about longjmp? This is not a trick question: a lot of code out in the wild uses longjmp.
The Go standard library uses panic/recover as a control flow mechanism in parser code for example.
I do the samething in the Pushup parser. With a recursive descent parser, since you’re using the host language’s call stack as a data structure, it’s much simpler to panic with a syntax error in a controlled way than mix “regular” error values with syntax errors and manually bubble them up.
This depends on use-case, but for langauge-tooling parsers, what I often do is treating syntax errors not like host-level errors, but as normal domain objects. So, a parse function produces a value and a list of errors. So, the “failing” function just consumes some input and pushes the error to the list of errors. And then some top-level while not end-of-file loop bails naturally when the all the input is consumed.
Make sense, to not have syntax errors affect control flow while parsing (at least in the sense of whether to exit parsing early). That seems a more modern way of parsing especially as you mention due to language tools. Will look into that for Pushup, since an LSP is on the horizon.
But i also think it’s pretty smart to not name it “exceptions”. Everyone knows to throw exceptions just about anytime, but they tell me not to use this panic() thing, and instead every example and function signature I can find returns an error.
If they didn’t, there would be a lot more panic()s in the wild.
My 2c in the discussion: yes, Go have exceptions (I always thought so ever since I heard about panic and recover), they’re rarely used in real code and their best use case is more akin to assert in e.g.: Python, where if you are panicking there is probably no way to continue the program safely (you can choose to do so, like calling assert, but generally it is a bad idea).
For example, I use panic in this program because it never makes sense for someone to call this function with an empty string as the command parameter. prepareRequests is private and it is called by other functions that are public, so the panic in this case is mostly to help someone implementing a new method to not do the wrong thing.
Assert and unreachable can each be defined in terms of each other:
Assert: if (!ok) unreachable;
Unreachable: assert(false);
Therefore, according to Go language designers, you’re not supposed to do that:
Go doesn’t provide assertions. They are undeniably convenient, but our experience has been that programmers use them as a crutch to avoid thinking about proper error handling and reporting. Proper error handling means that servers continue to operate instead of crashing after a non-fatal error. Proper error reporting means that errors are direct and to the point, saving the programmer from interpreting a large crash trace. Precise errors are particularly important when the programmer seeing the errors is not familiar with the code.
We understand that this is a point of contention. There are many things in the Go language and libraries that differ from modern practices, simply because we feel it’s sometimes worth trying a different approach.
So basically the nonexistence of unreachable is an experiment. I think it’s a terrible idea, obviously, which is why Zig indeed has an unreachable keyword.
I think it’s extra silly because to me a stack trace is much more useful for debugging a logic bug in the code than some kind of user-facing error message that managed to bubble up to the surface, hopefully without corrupting state along the way.
To give the Go designers credit, I do see some people abusing unreachable in Zig sometimes, but I see it as an education problem, because the concept transcends programming languages. It’s even relevant in Go despite the efforts to pretend otherwise, as evidenced by your practice (and mine, and many other people’s) of using panic("unreachable") in Go code.
this falls pretty squarely in the category of “technically true but missing the point”.
Is Go’s panic/recover mechanism fundamentally exception-like in nature? Yes, it is. Are there codepaths that exist in which panic/recover is used for error handling? Yes, there are.
Those cases are few and far between, they’re not the dominant and main mechanism for error handling. The stdlib and the ecosystem uses the
error
interface and multiple return for error-handling in the vast majority of error-handling codepaths, to such a degree that it’s conventional wisdom that a package should not panic and expect that a caller callrecover()
.It would be strictly more accurate to say “in Go, errors are nearly always signalled through values that use the
error
type, which is an interface, which is a boxed fat pointer on the heap, and represents a node in a tree oferror
values that is traversable using the functionality provided by theerrors
package, except in a few situations where they usepanic
andrecover
, but generally speaking it’s unlikely you’ll ever encounter a library that callspanic
and expects you torecover
”, but that’s a lot of words, so we just say “Go doesn’t use exceptions for error handling”, because for nearly every person in nearly every situation that’s the useful level of accuracy that people need when they are learning the language and not fretting about every corner case up front.The point is when you see this code:
In Zig it is correct code. In Go it is incorrect code.
Go and the ecosystem usually don’t focus on correctness, and are fine with 90% solutions. So the fact that there’s a corner case (
bar()
panics) wouldn’t be a concern.You’ll just get a crash and a new K8s pod, so don’t worry about it ;)
An attitude like that makes me concerned that I can never rely on ordinary Go code.
I think ordinary Go code doesn’t run in critical contexts, so that attitude is mostly fine in practice. You just don’t want to use Go outside its target/niche.
From a design perspective, Rob Pike infamously (basically) said Googlers are not to trust with anything advanced like generics or iterators; that’s for researchers.
And simplicity is the Go ethos, even at the cost of trading off correctness in some cases.
I personally find that mentality condescending and sad as a programmer that likes to perfect their craft, and robust code, even if just to explicitly acknowledge something’s not handled.
That being said Go is not all bad, my favorite things about it are cooperative cancellation via Context, and how tooling just works*.
I do have a lot of critical opinions about it because I’ve used it a lot and my values don’t align.
*minus the part about proxying every dependecy download through Google, and the compiler downloading and running newer versions on the fly, both showing the Google influence/values again
The net/http library uses recover by default, as do many other libraries, so you may not get a top level crash as you describe.
Yes
recover()
is sprinkled at appropriate boundaries, such as request handling fornet/http
, and that’s definitely a good thing.There’s also cases where you can’t
recover()
: some runtime behavior is a “fatal error”, not a panic. For instance unlocking an unlocked mutex, or concurrent writes to a map.I’m not against
panic
at all, it makes sense in most languages, and definitely in Go’s target use cases.That doesn’t conflict with my opinion that “usually don’t focus on correctness” is a fair assessment. The rest of my comment is maybe a bit too snarky because I was feeling jokey.
Will the pod spawn though? Cause k8s is written in golang…
Can you explain where the problem is here?
If
bar()
panics, thenx.a
has been modified butx.b
hasn’t.A correct form would be:
Shit I never considered that.
That’s the reason why Rust’s stdlib locks support poisoning.
TBF without effects it’s mostly an annoyance / drag.
Yeah on this, Rust added support for unpoisoning mutexes so you can fix state
In practice, there is no problem.
In theory, a panic may happen so that bar() and/or unlock() are not called.
But
unlock()
is called if the program panics: https://play.golang.com/p/XiCz846Se3gSo I guess that means the problem is that the code doesn’t set
x.a
back to what it was originally, as though in a database transaction, in the case thatbar()
panics?If
bar()
panics, in 99% of cases we should be letting the program crash so partial update of an object doesn’t really matter anyway. Go is being pragmatic here.Yeah, I think the problem arises if someone calls
recover()
in thedefer
, then they end up with broken state (like so).Ah, good point. You are right!
This was my thought as well. Correct me if am wrong but shouldn’t we be aware enough to handle this case when we implement it like that? Or the requirement is that the compiler should tell it?
“Go doesn’t use exceptions for error handling” is a lie, regardless of how convenient of a lie it is. A newcomer to the language is building a knowledge foundation that is wrong if presented with such statement.
Just say that there are exceptions but should almost never be used because reasons X Y Z.
Well it doesn’t use exceptions for error handling by convention. But you can, and it does have exceptions. I think the statement is still fair.
If the goal is just to reduce useless Pull Request by pedant, I’d suggest using language like “C++, D, and Go use control flow mechanism like throw/catch or panic/recover, which can prevent bar() from being called”. I believe it would get the message across without triggering tedious semantic argument about what is the one true definition of “Exception”.
Its easy to have conflicting opinion on whether or not panic/recover is about “exception”, and if it is or not a good practice to use it, but it’s a hard fact that it exists in the language and that it is control flow.
Agreed. It seems like the motivation of the Zig documentation is to show that Zig has no hidden control flow - which is great! Maybe it’s better to avoid comparisons to other languages in that respect. It’s not a zero-sum game, there’s no need for value judgements.
Agreed. Seems more pragmatic to do that simple change than referring to a blog post every time someones mentions it.
Hard disagree.
When I see
panic
->recover
used explicitly as an error mechanism in a module I stay the hell away from it. A little more acceptable is having arecover
when executing code that you don’t own, like go-chi’s Recoverer middleware, but even that I only use in development environments.So, you avoid using net/http, right?
EDIT: this was unnecessary snarky, sorry! What I actually wanted to say is that panic in the parser cited by kristoff is kinda an odd thing, but having a catch-all recovery around an HTTP handler is a central example, in any ecosystem, of why something similar to unannotated unwinding is necessarily useful.
There’s a long tradition of bailing out of recursive descent parsers using longjmp or exceptions. Doesn’t strike me as odd that the author chose touse the same design pattern when writing a parser in Go.
thank you for the example, linked it in the post
Erm, I feel like this serves an almost identical use as go-chi’s recoverer, which I already mentioned as an exception. Perhaps it would have made a better example.
I thought I was clear that what I meant was a module using panic and recover explicitly for some internal error handling.
The “development environments” part threw me off then. It seems that than this is mostly a terminology question: can the thing that the go’s net/http does around user-supplied handler be called “error handling”, or is it something else? (I personally would really love for this to have a separate word, but in the absence of one, I’d use “error handling” to describe what go does there, although that is clearly a different error handling from, eg, parsing a decimal number when the input is
"❤️"
)I think of it as an “error boundary”. I think I got the term from React. It’s an attempt to isolate the crash to a specific request / thread / user / region of a page.
It definitely is error handling, of the funkiest kind because it focuses on otherwise “unhandled” errors and turns them into handled ones, which works up until it doesn’t, since sometimes the unhandled-ness of the original error can’t be fully contained in the blast radius under the recover.
The talk section I linked in my other comment shows in my opinion a very compelling example where this is exactly what you see (trying to handle unhandled errors, and Go failing at that), and it’s not that hard to imagine even non-google-scale code eventually running into similar problems when dealing with any form of data that lives past the http request-response cycle (e.g. caching).
Including the standard library? Because running Go in an environment where panic=abort will introduce you to all the parts of Go’s stdlib where panic/recover is used in place of
return err
.To followup on this, I also disagree with the main thesis of the article: Panic/Recover is NOT exception handling because exception handling (at least to me) implies the existence of a typed recovery mechanism, which Go doesn’t have.
That’s one way of looking at it, for sure. To me a much more defining characteristic is the fact that you have to be worried of leaving corrupted state around because a function panicked and then somebody up the stack recovered leaving some fields unset, which is a trait shared by both try/catch and panic/recover.
The talk I mentioned explains this problem really well https://youtu.be/GtsSzbs-xb8?si=NXGA6npxivEgH-gi&t=2005 (timestamp)
FYI, in C++, where exceptions are the norm and not the exception (at least in some codebases; pun intended) this property is called “exception safety”: leaving objects in a valid state in the face of exception-caused stack unwinding. It takes some time and effort to get a hang of writing exception-safe C++ code. And I agree with you, allowing exception-like unwinding without a concern for state is asking for trouble.
It is typed tho, you just do it yourself. It definitely works and you should definitely check the types that you recover so you can reraise ones that shouldn’t be caught (like runtime errors).
OK, fair. I should have been clearer: “an explicitly typed recovery mechanism”.
Does that mean JavaScript doesn’t have exceptions?
Do you really need to be this persnickety and obnoxious?
I’ve said this for a long time:
— From 2013
What about longjmp? This is not a trick question: a lot of code out in the wild uses longjmp.
Edit: note: here’s an issue: https://github.com/ziglang/zig/issues/1656
Fully agreed with the article’s premise.
If it panics like a duck, recovers like a duck, and looks like a duck, it might just be an exception mechanism. 🤷♀️
I do the same thing in the Pushup parser. With a recursive descent parser, since you’re using the host language’s call stack as a data structure, it’s much simpler to panic with a syntax error in a controlled way than mix “regular” error values with syntax errors and manually bubble them up.
This depends on use-case, but for langauge-tooling parsers, what I often do is treating syntax errors not like host-level errors, but as normal domain objects. So, a parse function produces a value and a list of errors. So, the “failing” function just consumes some input and pushes the error to the list of errors. And then some top-level
while not end-of-file
loop bails naturally when the all the input is consumed.Make sense, to not have syntax errors affect control flow while parsing (at least in the sense of whether to exit parsing early). That seems a more modern way of parsing especially as you mention due to language tools. Will look into that for Pushup, since an LSP is on the horizon.
If you are looking into LSP, then let me plug https://matklad.github.io/2023/05/21/resilient-ll-parsing-tutorial.html as well, as it sounds like it could be useful here!
The pedantic is strong with this thread.
Go has exceptions the same way as humans have tails. Technically true, practically false.
Agree!
But i also think it’s pretty smart to not name it “exceptions”. Everyone knows to throw exceptions just about anytime, but they tell me not to use this panic() thing, and instead every example and function signature I can find returns an error.
If they didn’t, there would be a lot more panic()s in the wild.
My 2c in the discussion: yes, Go have exceptions (I always thought so ever since I heard about
panic
andrecover
), they’re rarely used in real code and their best use case is more akin toassert
in e.g.: Python, where if you are panicking there is probably no way to continue the program safely (you can choose to do so, like callingassert
, but generally it is a bad idea).For example, I use
panic
in this program because it never makes sense for someone to call this function with an empty string as thecommand
parameter.prepareRequests
is private and it is called by other functions that are public, so thepanic
in this case is mostly to help someone implementing a new method to not do the wrong thing.I’ve used panic(“unreachable”) at the bottom of a function to communicate the equivalent of Rust’s unreachable macro.
Assert and unreachable can each be defined in terms of each other:
if (!ok) unreachable;
assert(false);
Therefore, according to Go language designers, you’re not supposed to do that:
source
So basically the nonexistence of unreachable is an experiment. I think it’s a terrible idea, obviously, which is why Zig indeed has an
unreachable
keyword.I think it’s extra silly because to me a stack trace is much more useful for debugging a logic bug in the code than some kind of user-facing error message that managed to bubble up to the surface, hopefully without corrupting state along the way.
To give the Go designers credit, I do see some people abusing
unreachable
in Zig sometimes, but I see it as an education problem, because the concept transcends programming languages. It’s even relevant in Go despite the efforts to pretend otherwise, as evidenced by your practice (and mine, and many other people’s) of usingpanic("unreachable")
in Go code.