Skip to content(if available)orjump to list(if available)

The cost of Go's panic and recover

The cost of Go's panic and recover

180 comments

·March 1, 2025

rob74

> panic and recover are best reserved for exceptional circumstances.

You might go with Joshua Bloch and say exceptions are also best reserved for exceptional circumstances (which actually only means "things aren't working as expected"), that's why Go's authors used "panic" instead of "throw" or something similar, to make clear that it shouldn't be used where you might use exceptions in other languages. I mean, it's in the FAQ too: https://go.dev/doc/faq#exceptions

aqueueaqueue

You know why I hate exceptions most?

When debugging be it C# or JS, neither the "break on all exceptions" or "break on caught exceptions" are useful on any app. One just hits random library shit, or whatever bloat is in the codebase all the time and the other won't break at all.

But because exceptions are the control flow that is the only way to debug them (or do a human binary search)

Not sure what go debugging is like but I imagine you can quickly work your way to the first err!=nil while debugging.

mrighele

I don't know about C#, but in my Java IDE when I set a breakpoint on an exception I can set a filter not only on the class being throw, but also on the class that catch it, the one that throws it and the caller method, and to trigger only after another breakpoint is hit or it is the nth times it has been passed. With this you can make it trigger only when needed in a farly easy way

CharlieDigital

It's the same for the mainstream debuggers for .NET.

rs186

Well, "break on exceptions" can be very powerful when used correctly, i.e. when the scope is narrowed down. It should never be a flag that is turned on all the time -- that's guaranteed misery there.

> quickly work your way to the first err != nil while debugging

I doubt you'll spend any less time debugging in Go. If you disagree, I'd love to see a "side-by-side" comparison for code that's functionally the same but written in both Go and JS, and see some explanations why it's easier in Go

BobbyJo

I've shipped code in JS, Python, C++, Java, Golang, and a few others. I can say with certainty Golang was the easiest to debug simply because if a layer could fail, it was explicitly obvious. Exceptions might come from a few layers deep, and you don't know that until one gets raised.

Someone

> Not sure what go debugging is like but I imagine you can quickly work your way to the first err!=nil while debugging.

How do you imagine that happening? I can’t see another way then either stepping through your code or setting breakpoints on all the ‘return nil, err’ statements. You rarely, if ever, can use a ‘watch variable’ feature, because each function will have its own local ‘err’, and will have a new one on each invocation.

If, instead of ‘return buil, err’, there’s ‘throw new…’ in such blocks, I don’t see why you couldn’t do the same things.

jcelerier

That's very opposite from my experience in c++. Enabling break on throw in gdb or lldb always brings me exactly where I need to be no matter the OS / platform. But the software in c++ pretty much always adheres to "exceptions only for exceptional circumstances" and thankfully let them bubble up without rethrow à la java, otherwise it would be absolutely terrible developer ux

7bit

You can limit those to code you write. But it sounds like you break also on code that you didn't write eg, libraries or modules. Of course you're miserable.

aqueueaqueue

Tell me the dev console option to not do that and I'll use it. I admit it has been a while since I front ended.

sapiogram

The go standard library also recovers from panics internally in some places, for example for gradient descent parsers.

wetpaws

[dead]

9rx

> which actually only means "things aren't working as expected"

Exceptional circumstances, or exceptions for short, mean "things aren't working as expected due to programmer error". In other words, a situation that theoretically could have been avoided by a sufficiently advanced compiler but that wasn't caught until runtime.

"things aren't working as expected" is vague enough to include errors, which are decidedly not exceptions. One might say a hard drive crash or the network failing isn't working as expected, but those situations are not exceptional.

> to make clear that it shouldn't be used where you might use exceptions in other languages.

Other languages are starting to learn that you shouldn't use exception handlers where you wouldn't use panic/recover, so I'm not sure there is a practical difference here.

bazoom42

Terminology is a problem here. A crashed harddisk is clearly an exceptional circumstance. More specific terms is needed to distinguish errors in the code (eg divide by zero) from unpreventable errors like network failure.

9rx

> A crashed harddisk is clearly an exceptional circumstance.

It is clearly not. It is very much something to anticipate. At sufficient scale, it is guaranteed that it will happen. There is nothing exceptional about it.

> More specific terms is needed to distinguish errors in the code (eg divide by zero) from unpreventable errors like network failure.

Luckily we have such terminology already: Exceptions (caused by mistakes in the code) and errors (caused by external faults).

tored

Isn't that what we have exception hierarchies for?

daveliepmann

>Exceptional circumstances, or exceptions for short, mean "things aren't working as expected due to programmer error".

Interesting. In Javaland this describes assertions, and the term exception is for operating errors, i.e. problems not necessarily attributable to programmer error, including your example of a network failure.

ignoramous

> In Javaland this describes assertions, and the term exception is for operating errors, i.e. problems not necessarily attributable to programmer error

Making matters more confusing in Javaland, Errors are separate from Exceptions, but both are Throwables.

  An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it.
https://docs.oracle.com/javase/8/docs/api/java/lang/Error.ht... / https://archive.vn/i5F7B

trallnag

In Java I always see exceptions being used for stuff like validating HTTP requests

jjmarr

I've solved n-queens once before using exceptions to handle control flow. I coded a recursive solution for an assignment, but I wrote it wrong and it ended up printing all of the possible solutions instead of just one.

Because I didn't have much time before the final submission, I just put the initial call in a try catch block and threw an exception to indicate successful completion.

actionfromafar

Enterprise ready. :)

CJefferson

Honestly, this is the best way to write recursive algorithms in my opinion (and I write a lot of recursive search algorithms in my research).

The alternative is every single function has to return a boolean, along with whatever else it would return, which is true when you have found a solution, and you then return straight away -- effectively just reimplementing exceptions by hand, which doesn't feel like a useful use of my time.

rollcat

Sounds like Go to me! Every single function that can fail has to return an error value, which is nil if you have succeeded.

I love Go, but its error handling leaves so much to be desired. I even have an Emacs macro that inserts "if err != nil" for me. Also very easy to make a mistake in the rare case where you have to write "if err == nil"; my eyes just skip over any line that includes the words "if", "err", and "nil", as if it was some attention-hungry inline ad.

Rust started off chatty, but soon introduced the "?" syntax, which works perfectly well in the 99% case. Go had a couple of similar proposals, but "keep if err != nil" just... won.

bheadmaster

I like to use Go error handling for providing curated error messages for each possible point of failure.

Stack traces in other languages do this in a way, but are often unreadable and require manually inspecting each function in the trace to figure out the reason of the bug. Even worse if some function in the middle catches the original exception and returns another exception, completely devoid of the former's context and information.

Even worse if you're using any kind of framework that renders stack traces useless ala async Rust or Java Spring. All you get is a bunch of noise from framework and almost nothing from your program.

In Go, most of the errors I get are chains of carefully written messages, e.g. "request failed: write file: create: directory does not exist". And given that errors are just values and not tied to a stack, they can internally be passed through channels in all kinds of complex goroutine pipelines, and not lose any information.

Go basically makes your life worse, until you get used to writing meaningful context-relevant messages and thinking about errors. Which in turn makes your life a lot easier when diagnozing and debugging an issue.

9rx

> which works perfectly well in the 99% case.

Not without its related traits and whatnot. Without those you have the same all the same problems the Go proposals keep running into. Trouble is that it is not yet clear what the Go equivalent is to those related features.

However, the attempts to figure that out keep coming. Ian Lance Taylor (of generics fame) recently implemented the latest proposal to try it out, although it seems it too has failed, but suggests that things are inching closer to something usable when the initial details were able stand up to basic scrutiny. Nothing has "won" yet.

stouset

I remember when Rob Pike wrote an essay insisting that `if err != nil` isn’t all that common and golang programmers clearly would wrap that logic in more fluid interfaces which would prevent the redundant boilerplate.

kccqzy

That's why in Haskell after the Either monad becomes popular, people simply made a library that flips the arguments to become the Success monad.

The problem with most languages here is the name "exceptions" implying it's for exceptional scenarios, but without any substitute for good non-local control flow.

layer8

It’s not necessarily only for exceptional scenarios, but instead it is an exception to the normal return of a value. The usage where the return is always by exception (as in the n-queens example) is itself an exception ;). I do think it’s useful to mark one (return) type as the primary/normal/good case and other types as secondary/auxiliary/error cases, and have the latter auto-escalate independently.

tcfhgj

"The alternative is every single function has to return a boolean"

Rust: Return Some(value) or None

CJefferson

I tried that, the problem was some functions wanted to return an Option, then you end up with Option<Option<T>>, which is technically fine, but I find a bit mind bending.

vbezhenar

Another alternative is setjmp/longjmp.

kamaal

Its strange that its 2025 and we haven't compiled recursion as a design pattern with rules, like a framework, using which all problems that can be solved by recursion can be represented.

shric

I programmed in Go for 5 years (stopped 2 years ago) and didn't even know the language had recover() until 5 minutes ago.

I used panic() all day, but never recover. I use panic for unrecoverable errors. I thought that's why it's called "panic".

usrbinbash

> 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.

the_gipsy

You do need to use it, not to handle errors but to avoid it taking down the whole process (and probably sending some logs / alert / monitoring). Which doesn't apply everywhere, but at least in web dev it does: if a request / task panics, you want to abort just that, not the whole server including any other requests / tasks running.

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.

gizzlon

That's not my experience. Other then the recover included in the http lib, I don't think I have ever used recover.

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.

usrbinbash

> if a request / task panics

...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.

lblume

Exactly, same as panic! in Rust.

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).

kmeisthax

Also (3) there are panics that can't be safely caught, such as stack overflow.

vbezhenar

panic/recover is the same thing as exceptions. You can avoid them if you want, that's your decision, doesn't change the technical fact.

usrbinbash

No it isn't. Semantics matter, and using something against the defined semantics of the language is a huge code smell.

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.

williamdclt

`recover` is still useful for unrecoverable errors, eg to capture telemetry then propagate the panic again

stouset

Sometimes you don’t even want to recover, just do something like log it remotely so it can be seen and debugged.

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.

DanielHB

In my old project we used recover to give a HTTP 500 response, trigger a log/alert and restart our HTTP router and its middlewares in case of panic in some function.

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)

bilekas

Maybe I’m mistaken but isn’t that a hand crafted attack vector to taking down your services?

knome

hopefully, anything triggering a panic should be an exceptional case that the caller cannot purposefully put the server into. restarting the app without a full process reload shouldn't be any more of an attack vector for denial of service than restarting the entire app.

tgv

There may be libraries that call panic. E.g., the templating library does that. In that case, I want something in the logs, not a termination of the service.

gizzlon

IIRC, the Must.. functions are typically used at program start, and in cases where you would like the program to stop. At least that's the way I've used it.

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.

https://pkg.go.dev/html/template@go1.24.0#Must

arccy

the template and regexp packages have Must variants that panic. They're intended to crash the program on init because of programmer error.

amanj41

I think a good usecase for recover is in gRPC services for example. One wouldn't want to kill the entire service if some path gets hit leading to a panic while handling one request.

closeparen

Corporate gRPC services are written with "if err != nil" for every operation at every layer between the API handler and the db/dependencies, with table-driven tests mocking each one for those sweet sweet coverage points.

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.

GeneralMayhem

One thing that `if err != nil { return err }` lets you do, which panic/recover doesn't, is annotate errors with context. If you're throwing from 5 layers deep in the call stack, and two of those layers are loops that invoke the lower layers for each element of a list, you probably really want to know which element it was that failed. At that point, you have two options:

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)
    }
  }

amanj41

Yes that is common. I was more talking about the case where someone perhaps introduces a bug causing a nil pointer dereference on some requests, so the panic is not explicitly called in code. In which case you would definitely want the recover in place.

seanw444

Some are of the opinion that that should be handled a layer up, such as a container restart, because the program could be left in a broken state which can only be fixed by resetting the entire state.

oefrha

Given that you can’t recover from panics on other goroutines, and Go makes it extremely easy to spawn over goroutines, often times it’s not even an opinion, you have to handle it a layer up. There’s no catchall for panics.

troupo

In most software there's no such thing as unrecoverable panic. OOM is probably the only such error, and even then it doesn't come from within your app.

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.

Ferret7446

Go never had a stance on "unrecoverable panics kill your program". Go always supported recover, but encourages (correctly IMO) error values because they are more performant and easier to understand. The Go standard library even uses panic/recover (aka throw/catch) style programming in specific instances.

troupo

> they are more performant and easier to understand.

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.

usrbinbash

> In most software there's no such thing as unrecoverable panic

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?

troupo

Note how none of these issues should cause the respective programs to crash, as this is what `panic` does.

They try to start, cannot do a specific operation, and they do an orderly shutdown. Or they should

shric

For me an unrecoverable error is when my program gets into an unexpected state. Given that I didn't anticipate such a thing ever happening, I can no longer reason about what the program will do, so the only sensible course of action is to crash immediately.

bilekas

I have to add the obligatory “you’ve solved the halting problem” question here.

troupo

No idea what this has to do with the halting problem

donatj

I write a pretty significant amount of Go code for my day job, and I have written code that calls panic probably less than five times.

There are only really two types of cases where I would even consider it an option.

Firstly, cases where I am handling an error that should never ever happen AND it is the only error case of the function call such that eliminating it removes the need for an error on the return.

The other case is where I have an existing interface without an error return I need to meet and I have a potential error. This is the result of bad interface design in my opinion but sometimes you have to do what you have to do,

KingOfCoders

I only use panic during startup on unrecoverable errors, can't read credentials for example.

hanikesn

How often did you access a nil pointer and caused a panic?

donatj

Well I meant code that panics intentionally, via a call to panic, I've certainly written my share of off-by-ones ;)

ThePhysicist

Do you use recover() a lot? I have never used it much, I guess it is important in some cases but I don't think it's used that much in practice, or is it?

supriyo-biswas

The only use for me has been to put a recoverer middleware[1] to catch any unhandled panics and return HTTP 500s in my applications.

[1] https://github.com/go-chi/chi/blob/master/middleware/recover...

smnscu

Having used Go professionally for over a decade, I can count on one hand the times I used recover(). I've actually just refactored some legacy code last week to remove a panic/recover that was bafflingly used to handle nil values. The only valid use case I can think of is gracefully shutting down a server, but that's usually addressed by some library.

rgallagher27

I've "used" it in pretty much every Go project I've worked on but almost always in the form of an HTTP handle middleware. Write once, maybe update once a year when we have a change to how we report/log errors.

colonial

At least in Rust (which has an effectively identical API here) the only reasonable use case I've seen is as a "last resort" in long-running programs to transform panicking requests into a HTTP 500 or equivalent.

goodoldneon

I always make sure recover is used in all goroutines I start. I don’t want a panic in a random goroutine to crash my whole server

lelandbatey

I've only ever had to use it in certain complex cleanup mechanisms (certain transaction handling, request middleware, etc)

commandersaki

I've seen panic/recover used a lot with recursive descent parsers.

OutOfHere

One of Go's problems, relative to Rust, is that error values of functions can be ignored. In rushed corporate code, this means that developers will inevitably keep ignoring it, leading to brittle code that is not bulletproof at all. This is not an issue in Rust. As for static analyzers, their sane use in corporate culture is rare.

Hendrikto

You can ignore errors in Rust too, just like any language. And people do, just like with any language.

treyd

In Rust you have to explicitly state that you're ignoring the error. There is no way to get the value of an Ok result without doing something to handle the error case, even if that just means panicking, you still have to do that explicitly.

In Go you can just ignore it and move on with the zeroed result value. Even the error the compiler gives with unused variables doesn't help since it's likely you've already used the err variable elsewhere in the function.

foobarbaz33

True, but running errcheck will catch cases where you accidentally ignore the error. Maybe not as good as having it built-in to the language like Rust, but the goal of error check safety is achieved either way.

And there's a few cases like Print() where errors are so commonly ignored you don't even want to use the "_" ignore syntax. Go gives you the poetic license to avoid spamming underscores everywhere. error linters can be configured to handle a variety of strictness. For non-critical software, it's OK to YOLO your Print(). For human-death-on-failure software you may enforce 100% handling, not even allowing explicit ignore "_" (ie become even stricter than Rust language default)

bigstrat2003

That's only the case if you consume the return value. It's perfectly legal (though it gives you a compiler warning) to call a function that returns a Result and never check what actually happened.

Cthulhu_

The question that's needed to ask is whether you'd like the language or its ecosystem to guard against these things, or whether you are a decent and disciplined developer.

For example, Go's language guards against unused variables or imports, they are a compiler error. Assigning an `err` variable but not using it is a compiler error. But ignoring the error by assigning it to the reserved underscore variable name is an explicit action by the developer, just like an empty `catch` block in Java/C# or the Rust equivalent.

That is, if you choose to ignore errors, there isn't a language that will stop you. Developers should take responsibility for their own choices, instead of shift blame to the language for making it possible.

TheDong

> For example, Go's language guards against unused variables or imports, they are a compiler error. Assigning an `err` variable but not using it is a compiler error.

Unfortunately, Go's language design also enables unused variables without any error or warning. They are only sometimes a compiler error.

Specifically, multiple return interacts poorly with unused variable detection. See:

    func fallable() (int, error) {
       return 0, nil
    }

    func f1() {
       val, err := fallable()
       if err != nil { panic(err) }
       fmt.Println(val)
       val2, err := fallable()
       fmt.Println(val2)
       // notice how I didn't check 'err' this time? This compiles fine
    }
When you use `:=` it assigned a new variable, except when you do multiple return it re-assigns existing variables instead of shadowing them, and so the unused variable check considers them as having been used.

I've seen so many ignored errors from this poor design choice, so it really does happen in practice.

lexicality

Whenever I see people appealing to developers to be "disciplined" I think about those factory owners protesting that they wouldn't need guards or safety rails if their workers were just more careful about where they walked.

If developers were more disciplined Go wouldn't need a garbage collector because everyone would just remember to call `free()` when they're done with their memory...

amenhotep

Rust genuinely will stop you, though. You can't take a Result<T> and get an Ok(T) out of it unless there's no error; if it's an Err then you can't continue as if you have a T.

It doesn't force you to do something productive with the error, but you can't act like it was what you wanted instead of an error.

TheDong

Yeah, but now you have to teach your programmers about generics and applicative functors.

I'm just a simple country gopher, but my brain isn't capable of understanding either of those things, nor are any of my coworkers, nor any of the people we hire, and it doesn't really matter how theoretically "nice" and "pure" your generics and results and monads are if us real professional programmers get confused by them.

Errors need to be explicit values, not magic monads. 'if err != nil' is good and easy to think about, 'res.map().or_else()' is incomprehensible line noise no normal programmer can understand.

https://paulgraham.com/avg.html#:~:text=The%20Blub%20Paradox

dontlaugh

However, you can make a call without assigning the result at all, entirely ignoring it. That isn’t possible in Rust.

crackrook

In my view the core problem is the lack of proper sum types. Using product types to represent results seems fundamentally wrong; the vast majority of procedures only have 2 possible outcomes -- they fail or they don't -- 2 of the 4 outcomes that the type system permits are (almost always) nonsensical. I don't see a good reason to design a modern language in this way, it feels less like an intentional design choice and more like a hack.

liampulles

I think the issue here is not so much that errors can be ignored in Go (sometimes one doesn't care if something worked, they just want to try), it's more that errors can easily be ignored by accident in Go.

I definitely have seen that, and sometimes have done that myself, but I have to say it hasn't happened in a while since the linting tools have improved

Thaxll

Rust values can be ignored as well.

There is no language to my knowledge that prevent you from ignoring values.

k_g_b_

Any language that implements linear types has values that can't be ignored and need to be passed to some "destructor" (could just be a deconstruction pattern assignment/similar).

Examples: Austral, some of these https://en.m.wikipedia.org/wiki/Substructural_type_system#Pr... in particular at least ATS, Alms, Granule, LinearML Though most haven't gone beyond research languages yet.

peterohler

My experience is quite a bit different. Of course the examples I would use are more like what you might expect in real code. The comparison should be against code that calls a function that either returns and error and checks that error or one that panics and recovers. The overhead of returning the extra error and then the conditional used to check that error is more than a panic on error and recovery somewhere up the stack. This was not true in the early days of go but it is true today.

It really depends on the code being written. Try one approach then the other and see if it works better in your situation. For the example in the article there is really no need for an error check in the idiomatic case so why compare that to using panic. If there was an error to check the result would be much different.

__turbobrew__

Here are my rules of thumb:

1. Only panic in the top level main() function. Bubble up all errors from sub functions and libraries to the top level function and then decide what to do from there.

2. If you want to offer a library function which can panic on errors, create two versions of the function: one which returns an error, and one which panics on an error and has a name which starts with ’Must’. For example Load() returns an error and MustLoad() doesn’t return an error and instead panics on error.

MassiveOwl

I use recover when i'm unsure on how reliable some legacy code is so that we can emit our telemetry and then exit gracefully.

enjoylife

Only at the very end does the article call out there is actually a performance aspect if you use panic and recover as intended.

> So it seems that panic and recover can be beneficial to performance in at least some situations.

Namely top level & centralized recovery handling. I'll also point out its important your panic recovery must happen in a deferred function if your kicking off new goroutines. For example, your server library probably has default panic recovery on the goroutines handling inbound requests, but any new goroutines you create as part of handling the request (e.g. to parallelize work), will not have this handling built in. See https://go.dev/blog/defer-panic-and-recover for more.

flicaflow

If I remember correctly, the original actor implementation from scala used exceptions for control flow internally. Blocking on input queues would have blocked a whole thread which doesn't scale for a paradigm which should allow you to run very large numbers of actors. So the exception was used to implement something like light threads. Luckily go solves this problem with go-routines internally.

L-four

Writing transaction code wrapping a user provided function which can error or panic is a real pain to get right. If the user code panics there is no error if the user code succeeded there is no error. So you have to call recover on success.