So I know that in general, Roc has decided not to have an explicit panic. I think that maybe this should be reconsidered.
As I have been working on a Dict implementation in Roc, I have had many cases where I have wanted to panic due to impossible states. For example, If I ever get an Err OutOfBounds in any functions in the dictionary, that is definitely a bug and I want to know about it. It should not be possible. Without panic, I have to decide what to do when I receive an Err OutOfBounds. I could sneakily force a panice with 0 - 1 leading to a really confusing error message. I could just return some form of default value. But if someone calls Dict.insert and I return the old dictionary, it makes no sense. I think that panic is important for finding bugs and representing potentially impossible states that the compiler is not smart enough to optimize away.
On top of that, messing around with panics can be a way to get llvm to do more optimizations. If every operation produces the same potential panic, they will potentially be able to be checked together and produce one check and jump instead of many. This is one of the things I had to fight with a lot when trying to get hot loop to run faster by merging jumps.
I feel torn on this topic. I'm open to it, but it's definitely a one-way street; once it's in the language, there's no going back. :sweat_smile:
if I'd never used Rust, I don't think I'd be open to it. But Rust is an interesting example of an ecosystem where culturally panic seems to be used sparingly in libraries, such that I don't really worry about libraries panicking on me when they shouldn't, and I can't remember really feeling burned by that in practice.
the biggest reasons I'm open to it:
I think we want to support that #2 situation someday anyway, because if my server pancs on an integer overflow, it would be awesome if I got a stack trace so I could see where in the code the panic actually happened...but without that stack trace, getting an automated report of "an integer overflowed somewhere in your gazillion lines of code" is not likely to lead that bug to get fixed ever :sweat_smile:
I'd love to hear anyone's thoughts on this!
I think there is a big developer experience aspect to this. Having a good story around irrecoverable errors and how to use them hygienically is important. If the answer is "there is no language-level way to have irrecoverable errors", I think folks will just end up creating their own ad-hoc versions like Brendan's 0 - 1, which has poor ergonomics from a code-reading and error-reporting point of view. Even programs that propagate errors all the way up may reasonably use ad-hoc panics for cases where the compiler is not smart enough to understand their control flow (for example in redundant branches, see #ideas > Narrowing types in when expressions, or something like compiling a regex you know is infallible).
Anecdotally, I have seen some classes where it makes more sense to panic than it does to propagate errors up:
I think there is space for both errors-as-values and irrecoverable panics. They solve different problems. There is the problem of "please don't replace your valued errors with panics (and vice versa)", but there is merit to both uses.
I think it is important that panic in Roc does not have a way to get caught. It should be an unrecoverable exception based on when an invariant is broken. Something that can only be dealt with by the platform, if at all. I think that if someone ever wants to catch a panic, the panic shouldn't exist at all.
Hopefully will be a clear indication of a bug.
If it turns out it can unlock more performance optimizations, e.g. via LLVM. (I'm actually somewhat skeptical that it can, but maybe there's a way!)
I am pretty sure that I can prove this true. One simple case: Make every operation that might panic (but you can guarantee never will) use the checked version of the operation. Make them all throw the same "unreachable" panic when the check fails. Now they all will be jumping to the exact same code block if they panic.
This enables llvm to merge the checks together if one check guarantees another one of the checks. llvm can't do that merging if the 2 operations lead to different kinds of panics.
This is one of the things I have run into with incrementing integers and using them to access a list. I know that neither will ever overflow or access out of bounds. If they both threw the same panic, they could be merged into one check. They don't so I always have at least 2 checks.
I agree that there should at least be a consistent story for this in Roc. If it has panics at all, then we're already well along that one-way street, and the only way to truly reverse direction is to remove the concept of panics entirely (instead favoring checked, wrapping, or other deterministic outcomes). I agree that panics, when they do exist, should be irrecoverable but stylistically used only to check invariants/impossible states. Some languages have pre-condition/post-condition clauses in functions that formalize panics into rather declarative, concise, and readable assertions (essentially a constraint language within the grammar); I don't know how well that'd fit the panics that Roc already has, but it could handily solve Brendan's needs, for example, without obfuscating the code by mixing invariant checks with semantic behaviors.
It's worth noting that pre-conditions should not be used to validate untrusted inputs, since Roc's type system is expressive enough, particularly with tags, to handle almost all such validation in a cleaner way. The pre-condition, however, would be used to validate the internal state managed by the module itself (i.e. of the Dict input); it may be possible that all pre-conditions could be represented as post-conditions instead, since if a module has encapsulated internal state, then only the module's functions can produce such values, thus state could be eagerly checked at the end of a function doing updates rather than lazily at the start of a function receiving the updated value. It's probably then also the case that these invariants could be expressed alongside the type definition itself instead of within functions (perhaps as an assume clause, similar to how where is proposed in #Abilities). Perhaps modules could have a formal bug-report destination concept which Roc could use to automatically provide the option to, after review, submit a report/issue upon detected issue. Presumably productionized/optimized builds would simply omit code that checks the invariants.
@Kevin Gillette Worth noting that currently the panics are in the compiler, either to verify invariants, like Ayaz described, or as TODOs where a feature needs to be added. None of these should be viewable to Roc users, once it's production ready
@Derek Gustafson I had thought there were user-visible panics in the language itself, such as for numeric operations that overflow.
I had missed that
Yeah, you can always generate a panic using integer over or underflow. In my case, I just set the dictionaries size to 0 - 1 which cause it to underflow
Haskell does this very nice from the ergonomics perspective.
They don't panic, they specify undefined behavior.
undefined type checks everytime
Maybe we can have whatever
undefined isn't as nice from a reporting perspective
It really helps as much as Debug.todo helps for sketching the code , but I think that we should not panic in any way.
I like rust's unreachable
Well, I've written it with a wrapper that prints a message before ~undefining~.
I also like unreachable because it is clearer about when it should be used.
It really helps as much as
Debug.todohelps for sketching the code , but I think that we should not panic in any way.
How is undefined different from a panic? What happens if it is reached? Does it just keep executing and hope nothing is wrong?
for sketching we have holes, the single _ character
much more convenient to type
undefinedtype checks everytime
so does panic! in Rust, and so would any kind of explicit panic in Roc
one thing that I think is fairly compelling is that if something fits all of these criteria:
...then panicking (when the application author is able to specify how to recover and explain what happened to the user) is arguably better than using something like Result for several reasons:
Result handling that will all lead to the same error being reported to the user anywayResult involvedof course, the temptation of having an explicit panic available is that, because it's more convenient in those scenarios, to choose to bail out and show the user an error more often - even in scenarios where if Result was the only option, you'd otherwise handle the error more gracefully than that
here i'm hopeful that our editor could make the Result approach easier
as in, "i want to make this function return a result, bubble that up to all call sites"
definitely! Although it still makes all the types more involved along the way
another thing to consider: the current design for panics in Roc is heavily performance-optimized for the case where they never happen.
A panic in a Roc application literally runs a function in the host called roc_panic and that's it. It's completely up to the host what that code does.
This will usually happen in the middle of a call stack. The host can unwind that stack using libunwind, but has no way to clean up all the heap memory that was in use.
So the only way today to avoid a memory leak on every panic is to have roc_alloc remember all the virtual memory pages where it was stored, deep clone the entire application state into a fresh virtual memory page, and release all the old ones back to the OS.
This is actually theoretically very efficient if you're a server, because everything is arena allocated anyway, so you just fire off a HTTP 500 response and throw away the arena
But for something like a GUI, a panic would actually result in cloning the entire application state, which would likely at least drop a couple of frames
(There's a theoretical way to get panics to walk the stack and decrement refcounts but it would be a huge, messy project and I'd prefer not to do it.)
Dropping a couple of frames on panic is honestly probably fine if they are extremely rare events, like actual "I thought this would never ever happen" things - integer overflows and such - but I think a valid concern is that explicit recoverable panics might result in people overusing them for coarse-grained control flow and ending up with slower and slower programs as their application state grows, and then spreading the word that "Roc is slow at scale, actually."
If the host halts the process when a panic happens though that's not an issue. I can't think of many examples other than a server-like process when a host would want to do something other than exit?
well in a UI application you'd probably want to let the user continue with what they were doing
like the operation they just attempted failed catastrophically, so you display an error, but then they should be able to press ok and try something else
Idk
If it failed with a panic, continuing probably doesn't make sense
If they failed with an error, continuing makes sense.
hm.. personally, it seems to me like that kind of pattern means errors should be used and not panics. In my mind the role of panics is entirely different than that of errors, and panics should only be used when there's literally no way to recover. If you can present an error and keep going you should send an error up
:point_up: That
Otherwise, there is no way for the host to know if the application is even in a usable state. The model may be total garbage is half changed to the new state.
e.g. so far I've not seen a rust panic being caught
that's a fair point
but I think a valid concern is that explicit recoverable panics might result in people overusing them for coarse-grained control flow but I think a valid concern is that explicit recoverable panics might result in people overusing them for coarse-grained control flow
I guess this is the meat of the problem right, how do you enforce that panics and errors are used for orthogonal reasons.. maybe there is a way to make it harder to use panics for error handling
I know it's possible, I just don't think it really happens in the wild
yeah
so, maybe that should be a big cultural thing then
panic should mean unrecoverable loss of state
so if a panic happens, what you're supposed to do is exit immediately, possibly after logging diagnostic info about what went wrong
should we call it something else then?
e.g. crash
Folkert de Vries said:
I know it's possible, I just don't think it really happens in the wild
(aside: rustc uses it to convert panics into errors :face_with_thermometer: )
could even go with unrecoverableError just to make it stick out and be long to type.
Would make things pretty clear.
From the platform perspective an unrecoverable application error may not always mean an unrecoverable platform error. So I think this all makes sense. It means consider any application state and memory as garbage, but feel free to continue if that doesn't matter to the platform/the platform knows how to reset.
I'm fine with panic meaning unrecoverable, particularly if, in practice, the host can know how to deallocate everything related to Roc, in case Roc is used in an embedded way.
Panic will get misused if it's recoverable within Roc itself. If unrecoverable, it should only be used for programmer mistakes
in Elm it was called crash back when it was allowed outside release builds (which it no longer is), and that was how people used it
crash was responsible for NoRedInk's one instance of having a production runtime exception in 7 years, so I guess that's not a bad track record :laughing:
Panic will get misused if it's recoverable within Roc itself. If unrecoverable, it should only be used for programmer mistakes
unfortunately there's no real way to prevent this
a platform author can always have the application author provide a function to run if a crash happens, and then have the host call that function from within roc_panic
there's no way to rule it out, since hosts can call whatever functions they like
Richard Feldman said:
a platform author can always have the application author provide a function to run if a crash happens, and then have the host call that function from within
roc_panic
Sure, but that's categorically different than a recovery feature provided by and within the language itself.
As a general question, was anyone ever suggesting adding a recovery feature into roc itself? I am only try to get some sort of user exposed panic feature. No want for recovery.
The only time so far that I've wanted a Roc panic/crash was to assert the safety of literals (which I think qualifies as catching "programmer mistakes" as mentioned above).
» x = 5
… x % 3
Ok 2 : Result (Int *) [ DivByZero ]*
» x = 5
… x % 3 |> Result.withDefault 666 # No need for error handling, so let's just get use the Ok value. However, this is smelly.
2 : Int *
» x = 5
… modSafe = \a, b ->
… a % b |> Result.withDefault 666 # Encapsulate the smelliness into a helper function.
… modSafe x 3
2 : Int *
» x = 5
… modSafe = \a, b ->
… when a % b is
… Ok c -> c
… DivByZero -> crash "modSafe doesn't support b=0!" # Is this less smelly? I don't know!
… modSafe x 3
However, I can see this use case increasing flakiness. :shrug:
Richard Feldman said:
so if a panic happens, what you're supposed to do is exit immediately, possibly after logging diagnostic info about what went wrong
What about if you're running 3rd party Roc code? For example, plugins in an editor. That shouldn't bring down the whole editor.
Edit: It occurred to me that you're probably talking about just the Roc code, not the platform, so nevermind.
Perhaps Roc packages, once there's a package manager, can be automatically annotated with whether they panic, and maybe even distinguishing between panics for data they control (as in Dict example elsewhere, counter-balanced by test coverage) vs parameters you can pass in (such as panicking if you pass an even number). That could be used by users in search of a package to evaluate their reliability.
Ideally most explicit panics would be for a module's own types.
I could however see some cases where the function must only handle numbers of a certain range. For "must be non-negative" an unsigned type could be required, but occasionally there are stranger ranges ("between 3 and 127"). Some languages have ranged number types, but those haven't proved so valuable to have ended up in many languages. For Roc, the function could return a Result, or take an opaque type that would've been produced by another function returning a Result, or it could panic. I suspect panic would be quite disfavored here, but I also don't know in practice when too much use of Result or opaque types gets tedious.
While roc_panic does seem straightforward and reasonable for many platforms (where the host is just there to serve the Roc application), it seems plausible that there are some platforms in which roc_panic, at least afaict, doesn't provide great choices:
roc_panic call is not very convenient.All of these above examples feel less like a host <-> tenant relationship (where the Roc application has some expectation of exclusivity or prime importance), but instead more like a host <-> guest relationship (where this particular Roc code fits more into the role of a embedded scripting component, albeit faster and uninterpreted). Other terms certainly could be application (as is already used) and behavior.
These certainly may be misconceptions, but the [solvable] issues I see here with roc_panic are:
roc_alloc and friends will probably be naively implemented most of the time (iow by just calling the host's own allocator directly), thus without special effort, a host won't be able to distinguish between allocations made by different threads running Roc, or even between Roc-related and Roc-unrelated allocations, and thus a straightforwardly-implemented host will not free leaked allocations.Certainly a few of workarounds for memory leaks are for a host to give a separate arena to each thread/invocation, or to manually unwind the stack to find heap references (needing to know a lot of unsafe details about Roc in the process, and hoping its calling convention never changes).
I wonder if we could guarantee that, following a roc_panic in which the host does not call exit, Roc would unwind its own stack and decrement/free allocations, finally resulting in a sentinel "I panicked" value to the host. Roc could accept from the host a pointer to a signal variable (somewhat like errno) that it might overwrite before the Roc code returns control to the host. Alternatively, Roc could provide some utility functions to host that could be called within roc_panic, such as a function for filling a host-provided buffer with a formatted stack trace, and a function for unwinding the stack.
It seems like, ideally, hosts should need to know very little about Roc, and Roc should be able to clean up its own allocations even in the case of a panic (as it cleans allocations when no panics have occurred).
there were related discussions about this in https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/Explicit.20Panic.20in.20Roc
Ayaz Hafiz Thanks for the reminder!
Richard Feldman said:
(There's a theoretical way to get panics to walk the stack and decrement refcounts but it would be a huge, messy project and I'd prefer not to do it.)
Why would it be a huge mess? It seems like we could just arrange for each Roc stack frame to have heap-references at the "end" of the frame, along with an integer indicating the number of such references. At that point wouldn't the stack just be essentially a [contiguously-allocated] linked-list of frames, where the allocation-relevant parts, and the info needed to unwind, are all self-describing and size-agnostic? It might be a deep change, and there are probably some technical road-blocks, but, granted coming from someone with no specific experience in this area, it does not sound particularly intensive from an algorithmic sense.
so libunwind has a system like this, and LLVM is aware of it and can emit this info
I got a "hello world" example with clang and libunwind working some time ago, where it's at least able to walk the stack and print out the function calls
however, libunwind just lets you unwind the stack, it doesn't really ship with a way to say "okay at each point in the stack, here's some extra info that you need to know about how to clean up heap stuff for this stack frame"
C++ exceptions let you do that, and LLVM knows how to emit those too
in fact, we used to do this for panics in Roc
it was very complicated and caused all sorts of problems, including:
catch keyword? We used to deal with this by having every call from the host to a roc application function always return a Result, which was basically us wrapping every call to a roc app in a giant try/catch, and then returning an Err from the catch and an Ok if a throw didn't happen. This meant every Roc application had to do a conditional to check for potential panics on every call, whereas today that's no longer necessary. (Granted, we could theoretically change to the API of today and have the catch invoke roc_panic.)throw I'm aware of is in libc++ (the function LLVM emits is called __cxa_throw or something), so we used to have to link all of libc++ into every roc application in order to support this. Theoretically we could extract just what we need from libc++, port it to Zig, and include it in the builtins, but I took a look at it and concluded I don't know C++ nearly well enough to do that.Thanks for the context!
I didn't realize that made that many assumptions about calling conventions and stack layout, but it makes sense in retrospect that it would.
Linking against libc++ for just this reason certainly doesn't seem like a win.
In theory, how hard would it be for Roc to provide its own arena allocation implementation? iow, Roc still calls roc_alloc, but instead of asking for 4 bytes here or 32 bytes there, it just asks for page-size multiples. At that point it'd be a bit simpler for arbitrary platforms to ask Roc, as part of a roc_panic, to free the arena pages (presumably part of the first page could hold accounting that links to another pages).
This would still leave the question of reference data shared across the host <-> Roc boundary, but at least for hosts in which that's not a concern (such as with static data or where the counts can be reset back to 1), this would give arbitrary hosts a non-leaking recovery path.
hm, what would the benefit be of that compared to status quo? Hosts can already choose to have roc_alloc calls do arena allocation, and then the host knows all about the allocated memory and can deallocate as necessary
I'm suspect that providing an arena allocator is more work than many host authors (including those who want a recovery option) will want to spend time/complexity investing.
While some host languages no doubt will have an easier time of this than others, it's probably not a differentiating feature for many platforms, and thus leaves a poorer tradeoff for a host author that wants recovery, but doesn't have great arena allocation implementations in their language, or when there are options but the host author doesn't want to research them (dilemma of choice).
If Rust or Zig have good options (performant and robust), presumably we could provide that as an option to host authors via some pragma or build option. We wouldn't need to implement our own arena allocator, but merely layer a good preexisting one atop roc_alloc at the host author's request.
It should just be importing a rust library for the most part. I feel most of the basic roc_* functions could be implemented in a library
I think this can be supported rather simply on the platform side with a minor ecosystem. So using rust/zig/etc package manager instead of building it into roc
I'm suspect that providing an arena allocator is more work than many host authors (including those who want a recovery option) will want to spend time/complexity investing.
I think that this is a bad assumption based on the vision of Roc. Most roc users will not be platform authors. Platform authors, especially those implementing core platforms that many people depend on will need to deal with low level implementation details. This is not something most people have to think about at all, but some people do.
Last updated: Jun 16 2026 at 16:19 UTC