I used panic() all day, but never recover. I use panic for unrecoverable errors. I thought that's why it's called "panic".
I used panic() all day, but never recover. I use panic for unrecoverable errors. I thought that's why it's called "panic".
And you are exactly right.
The problem is: People are so used to the "exceptions" paradigm from other languages, when they see "panic-recover" many immediately think "That's the same thing!!"
It isn't, because the only VALID usecase for panics is exactly what you describe: unrecoverable error conditions where terminating the program is the best course of action.
`panic/recover` used like exceptions is an antipattern, and one of the worst code smells in a Go codebase.
There is a reason Rust was reluctant to add std::panic::catch_unwind at first. The docs thus explicitly mention that (1) it is not a typical exception mechanism and (2) that it might not even catch panics if unwinding is disabled (common for embedded and restricted development).
Sadly, you need every goroutine to have its own recovery handler. This works well for your general request / task entrypoints, as there should only be one for each kind, but you need to watch out for any third-party libs spawning goroutines without recovery. They will take down your whole server.
...and the condition why it panics is not a situation that warrants a crash, then whatever is called upon handling that request is issueing a panic when it shouldn't.
The reason why some libs do that anyway is exactly what I describe above: because in many peoples minds panic == exception.
That's a logic error in the code and should get fixed. And one of the best ways to make devs fix things, is to let their application crash when something that shouldn't happen happens anyway, because then someone will start complaining why a service is unreachable.
TL;DR:
If some condition shouldn't crash a process, it has no earthly business causing a panic.
There will always be panics. You don't need to crash the thing to make devs notice, they're not idiots no matter what Rob Pike told you. You can alert and not throw out the baby with the bathwater. Nobody wants panics in their code, even if they're not crashing the whole world.
I don't think so. If I have to handle a panic, because otherwise my program no longer works, one of 2 things is true in the vast majority of cases:
- There is something seriously wrong with the program or its environment, causing it to panic
- There is something in the program issueing a panic when really it should return an error
In short: there should be no need to "handle panics"
Panics are irrecoverable conditions where its preferable for the program to crash rather than continue. If code panicks for any other reason, thats, in my opinion, wrong, and should be fixed. Panics are not the equivalent to exceptions, and error returns exist for a reason.
People who don't like that paradigm can always use a language that uses the exception-paradigm.
FYI the go std library recovers from panics when it spawns goroutines internally, in most cases.
All this has next to nothing to do with exceptions. Nobody is saying to use panics to pass errors or any control flow.
Why is you code panic'in? I would let it take down the process and figure out why. I have had backend programs set up to automatically restart, which can be useful. But I would treat any panic as a big deal.
For example, at my work, we have some nightly long running tasks. We don't panic every day. But from time to time, let's say once or twice per month, some code changes cause a panic. On that day, we don't want to kill the long running tasks for no good reason other than somehow indirectly making someone fix the panic. We have alerts for that, and we're all grownups.
Yes it is mutually exclusive. Something that doesn't kill the program, aka a recoverable ERROR CONDITION should not cause a panic, that's not what panics exist for.
Something that causes a panic without using the `panic` keyword, like an out-of-bounds read, nil-derference, etc. is indicative of a serious problem that should be fixed before the program is allowed to run again.
Can you explain why?
> a serious problem that should be fixed before the program is allowed to run again
Can you explain why the program should not be allowed to run again? Is this some sort of software spiritualism?
Because that is semantically what a panic means in Go. See the link to effective go I posted you elsewhere in this thread.
I am well aware that it can be used in other ways. Same as I can say "car" when talking about a mainline battle-tank. Sure, a car has an engine, runs on fuel and drives on land. There are similarities. The words still mean very different things.
And I am also sure there have been instances of someone using a tank to go order food at a drive-through. Doesn't mean that it is semantically correct to do so, or advisable.
https://github.com/search?q=repo%3Agolang%2Fgo%20recover()&t...
It just doesn't make sense to take down the whole server, including all requests / jobs in flight, because there's some nil deref or out-of-bounds. Yea, that thing has to be fixed, but sending a specific alerts is much better than indirectly alerting by taking the whole system down.
If you're using go for something non-web, then it may very well make sense to not have recovery anywhere. Except of course you do have some, in the stdlib. But you can apply it to your code, if you want.
But it can't be some universal pragma (or convention) in go, as it violates the stdlib.
Does any of that change the semantics of what a panic means, and how applications should therefore react? No. Does it make panics the equivalent of exceptions in Python semantically? Also no.
And this logic isn't limited to Go. Guess what, there are python libraries that use Exceptions for control flow. It certainly works. Does that validate using Exceptions as control flow elements? No, of course not. Why? Because that's not what an exception exists for semantically.
Panic-Recover cycles in go codebases are an antipattern, and unless I see an official statement by the people who make Go (who also write "Effective Go" btw.) saying otherwise, those are the semantics of the language.
3P code is a thing
>why
Sometimes there are edge cases with nil pointers that testing missed.
>automatically restart
What about all of the other requests in flight? Letting those fail because one request hit an edge case isn't great for a production service with high throughput.
For example, I could ignore the fact that Python has exceptions, and instead let functions return error values.
Would that work? Yes, absolutely, and I have seen Py-Codebases that do this.
Is it semantically correct? No, because in python, semantics dictate that error states are handled via exceptions, and that is the expectation everyone has when opening a python codebase.
When in Rome, do as the Romans do.
Result, Either, Expected, all have different names, but their semantics are all the same.
Panic and Recover may not be idiomatically used the same way Exceptions are used in other languages, but they share the exact same semantics of implicitly bailing out potentially multiple functions, going up the call stack until we Catch, or well Recover.
Sometimes it can’t reasonably be handled until some natural boundary. A server that handles multiple connections at once can produce an unrecoverable error handling one of those connections, but it still should gracefully close the connection with the appropriate response. And then not kill the server process itself.
Restarting like that was faster and more stable than crashing the whole thing and restarting the whole server. But it is a bit dangerous if you don't properly clean up your memory (luckily most APIs are stateless besides a database connection)
For example to read and parse expected templates from disk. If they aren't there, there really is no reason to continue, it's just very very confusing.
What I really want is either a way to recover panics from any goroutine, or be able to install a hook in the runtime which is executed when an unhandled panics occurs.
You can kind of fudge this by having the orchestration layer look at the exit code of the golang process and see if it was exit code 2 which is usually a panic, but I noticed that sometimes the panic stack trace doesn’t make it to the processes log files, most likely due to some weird buffering in stdout/stderr which causes you to lose the trace forever.
I would love a community norm that errors which fail the request can just be panics. Unfortunately that's not Go as she is written.
1. Pass a context trace into every function, so that it can panic with richer meaning. That's a right pain very quickly.
2. Return errors, propagating them up the stack with more context:
for i, x := range listOfThings {
y, err := processThing(x)
if err != nil {
return fmt.Errorf("thing %d (%s) failed: %w", i, x, err)
}
}That said... I did like a clever bit I did where you can use a sentinel error to filter entire segments of the wrapped errors on prod builds. A Dev build gives full error stacks.
For all "unrecoverable panics" you usually want to see the reason, log it, kill the offending process, clean up resources, and then usually restart the offending process.
And that's the reason both Go and Rust ended up reverting their stance on "unrecoverable panics kill your program" and introduced ways to recover from them.
They are more performant because Go decided to make them so. E.g. in Erlang crashing a process is an expected lightweight operation.
As for "easier to understand"... They are not when:
- your code is littered with `x, err = ...; if err != nil`
- it's not easier to understand when the code errors have to be dealt with on a higher/different level. The calling code isn't always the one that needs to deal with all the errors
Just a very random example (I literally just clicked through random files): https://github.com/kubernetes/kubernetes/blob/master/pkg/con...
Oh, look, you can't even see the logic behind all the `if err`s which do nothing but return the error to be handled elsewhere.
Line 143 - 182...
You'd think they'd come up with a short form for something that gets written so often.
return when err
return if err
return ?> err
return ? err
Still readable, still pretty explicit IMO.Go is just bad at handling errors. Exceptions are superior in every way.
return when err
return if err
return ?> err
return ? err
to: return when stackify(err)
return if stackify(err)
return ?> stackify(err)
return ? stackify(err)
I don't know; I'm just throwing ideas out there.I do agree that exceptions are just fine and it's not like you couldn't just catch and return exceptions if you wanted to like err.
And there's high chance the layer above is doing the same thing, and the layer above, until you actually get to actual error handling.
Rust at least recognized this and first provided try? and then the ? operator as a shortcut to this boilerplate
return when err
return if err
return on err> The halting problem is the problem of determining, from a description of an arbitrary computer program and an input, whether the program will finish running
They’re very much related to determining you’re gonna panic or not.
Webserver wants to start, binding port 443/80 isn't possible because another process holds that port.
Logging service wants to write to disk. The IO operation fails.
RDBMS want's to access the persistent storage, the syscall fails due to insufficient permissions.
How are any of those recoverable?
They try to start, cannot do a specific operation, and they do an orderly shutdown. Or they should
Which is exactly what panic does.
Panic is a built-in function that stops the ordinary flow of control and begins panicking... The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes.
--- end quote ---
This is far from orderly. For example, what happens to other goroutines?
I also like how golang docs literally describe using panics as poor man's exceptions:
--- start quote ---
For a real-world example of panic and recover, see the json package from the Go standard library. It encodes an interface with a set of recursive functions. If an error occurs when traversing the value, panic is called to unwind the stack to the top-level function call, which recovers from the panic and returns an appropriate error value
...
The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.
--- end quote ---
And I like how https://github.com/golang/go/issues/26799 describes that use of panic in the original version of this bog entry from 2010:
quote:
However, this is a poor example and a misuse of panic and recover as exceptions. See https://golang.org/doc/effective_go.html#panic
The example will be invalid soon with the release of go 1.11 as well. The panic/recover was removed from the unmarshal code. See master's
end quote.
The blog entry was later changed because this was fixed. It now refers to marshaling which, sadly, sill uses this mechanism.
The fact that this is still in the json package is a pain point, yes. Does it validate the use of panic as a form of flow control? No. Here is what "Effective Go" has to say about the topic:
https://go.dev/doc/effective_go#panic
quote:
But what if the error is unrecoverable? Sometimes the program simply cannot continue.
For this purpose, there is a built-in function panic that in effect creates a run-time error that will stop the program (but see the next section).
end quote.
And semantically, a panic doesn't even need an orderly shutdown. Again: A panic should ONLY be issued if the application enters a state where continuation of normal operation is not possible, and/or may even be ill advised. "Orderly" operations are, by definition, no longer possible at this point.