I've been thinking more about having a crash keyword (like panic! in Rust, except with a name that more strongly discourages reaching for it) since that discussion last week about why Roc isn't (and shouldn't be) total
I've convinced myself that the package ecosystem would most likely be fine in practice if the language had a crash keyword, because:
panic! but it's successfully become a cultural norm that packages typically base APIs around Result instead of panicking. I think this is in significant part because panic doesn't have try/catch, so Result is actually the nicest way to make an API where callers can do custom error handling. The same incentive structure would be true in Roc.unreachable equivalent is optimal for performance, and there's value in making that more debuggable than the "build your own crash using overflow" approachso I'm curious what people think of the idea of adding this to the language:
crash "uh oh" immediately ends the program, in the same way that a failed heap allocation or an integer overflow would (under the hood it would run the host's roc_panic, which we'd probably rename to roc_crash in this world)crash expression as an argument to a function or assign it to a variable. (Because this is basically always a mistake - e.g. Result.withDefault (crash "blah") will not do what you might think it would at first glance!)crash is always available, including in the package ecosystem (e.g. unlike Elm's Debug.crash) and there is no way to tell the compiler to give an error if any of your code uses it, or to segment the package ecosystem based on packages which do or don't use itwhat do people think about the idea of adding this keyword to the language?
Whew, this is actually not easy to answer.
Coming to Roc from Elm (with previous experience in Haskell and Rust among others), I really like the fact that it is hard to crash Elm.
The escape hatches in Elm are basically the same but I have not seen it abused there, and it does not seem to be common to run into stack overflows or infinite loops in practice. I am also not sure how Roc has more custom types that would need failing (or an unreachable) than Elm? I generally use a lot of custom types in Elm and being unable to fail in some cases more often than not made me aware of a weakness in my modeling.
So I was going to write my opinion against adding this keyword, but then...
Thinking about how Roc integrates with platforms, the escape hatches there are much simpler. As far as I understand it, there is nothing stopping you from implementing your own thisDefinitelyWontFail function that calls panic! in the background, and many operations might do this implicitly. Given this, it might not be bad to have one method to panic inside Roc, so the compiler safeguards you mentioned in your post can be implemented there, giving at least some guidance.
I do, however, think that this mainly applies to code written specifically for interacting with a platform. So I would suggest thinking about doing crash somewhat like Elm does ports. Allowed in application code, but not in library code.
This would give library users the confidence that their calls most likely won't spontaneously combust the runtime and, like in Elm, the downsides for modeling seem minor. This seems especially important for a language that might handle resources with the need for cleanup (say, open files) but does not support a drop mechanism for when the runtime panics.
Just to give a simple example of a library that would make sense to have crash. I have a Roc flat hash map library that attempts to implement the equivalent of the c++ absl::flat_hasj_map. This is so users could have the option to use an extremely fast hash map. The roc standard library does/will have it's own dictionary, but it will make specific tradeoffs around ordering and hash flooding that will effect performance in some cases. As such, a package with a hash map (or other similar data structure could be quite useful).
With a hash map library, there are many list looks up and checks that can never fail (assuming no bugs). So this is a library where crash makes a lot of sense. Without crash, there are 3 other options:
I think generally speaking, when I want crash in an application, it is because I don't feel like dealing with errors or bubbling up errors got complex (lists in lists with results). Occasionally it is a truely impossible state, but that is pretty rare.
When I want crash in a library, it is because a state is unreachable and it doesn't make sense to return an error. Also, handling the case would have more of a performance cost than crashing.
When I want crash in a library, it is because a state is unreachable and it doesn't make sense to return an error. Also, handling the case would have more of a performance cost than crashing.
This is my thought process too - either this, or the user of my API has used it in a way that breaks the API contract. A rich type system does it make it easier to express an exact API surface and helps make the barrier-to-entry for something like crash high, but in the end all type systems are over-estimations and so you can't always express exactly what you want without allowing for some wiggle room. The wiggle room is where you want to disallow usage at runtime by crashing
I do feel like the entry cost should be pretty high though. The compiler warnings/restrictions are a good idea, but I think it needs to be a social (ecosystem-driven) thing too to have a certain discipline about what crashing means.
Another advantage to explicitly crashing, in my eyes, is it makes the DX of handling states you thought were unreachable, or not exposed by your API surface, much, much easier to find. With something like crash the compiler can insert instructions to print back where a crash occurred, and why. By forcing an infinite loop or integer overflow to induce a crash, it's much harder to get that. You can get it to some extent with an expect, but it would be a silent notification by default - you'd need to set up log collection and alerts on logs to see when an expect did not meet its condition.
I would question whether an API contract should be an expect or a crash, but otherwise totally agree with the sentiment.
Does the type system know about crash? E.g. can I write this?
unwrap : Result ok err -> ok
unwrap = \result ->
when result is
Ok value -> value
Err e -> crash e
I think ideally we would have
crash : Str -> a
so @Tommy Graves your example would work only if err = Str. Or maybe there is some ability like Display (https://roc.zulipchat.com/#narrow/stream/304641-ideas/topic/display.20and.20log) for err, and crash is crash : msg -> a | msg has Display
worth noting you can kind of emulate this today by forcing an infinite loop:
crash : Str -> a
crash = \msg ->
expect msg != msg
diverge = \{} -> diverge {}
diverge {}
but this is silent
Oh yeah -- I was less interested in that part and more in making sure the compiler knows the branch with a crash doesn't affect the return type of the function
yeah if the return type of crash is an unbound type variable it adds no information so it won't make the return type any larger
» crash : * -> *
…
… unwrap : Result ok err -> ok
… unwrap = \result ->
… when result is
… Ok value -> value
… Err e -> crash e
…
… unwrap
<function> : Result ok err -> ok
yeah this is how Elm's old Debug.crash worked
I think it would be better to have it be a keyword instead of a first class function though
because it doesn't really work like other functions in terms of control flow, which makes it easy to accidentally make mistakes with (e.g. Result.withDefault (crash "blah"))
Other than making it easier to restrict where you can type it, a keyword vs function would not change the semantics though right
right
from a type system perspective I think we can treat it like a function with that type
what if you make the rule really strict, like you can only execute crash as a unary expression in a scope (that is, as the only expression in a function, a when branch or conditional branch)? I think that covers the cases you would actually want to use this.
that would mean you couldn't build up a message string using intermediate defs?
what if it was like expects:
crash
prefix = ..
msg = ...
Str.concat prefix msg
?
It's not as convenient but maybe it shouldn't be?
I think this is a good idea. It happens very commonly that e.g. something is constructed using a constant value as parameter, so you know beforehand whether the outcome will be OK.
I have no strong preferences over it being a keyword or a function. I'm not entirely convinced yet that restricting its usage to a particular place (with it as a keyword) really has enough advantages to make it worthwhile. If you have more examples of where it should/shouldn't be allowed to help user friendlyness, I'd be very interested in them.
I do think that we need a longer and more scary name than crash.
I've always liked the name accursedUnutterablePerformIO for example, (though arguably it has gone slightly too far in its naming :sweat_smile: ).
That you write panic! in Rust rather than plain panic is another example of helping readers to see that there is something special going on here and that it should not be taken lightly.
What about a name like unreachableOrCrash?
A related thing to think about: When we introduce this, we inevitably need a way to recover from them. Not for everyday code, but for:
I think roc_panic is where the host can handle recovery (e.g. in the web server scenario) as needed. Imo the roc programmer should not have any way to manage recovery here
Also not for testing?
Probably roc expect can tell you when a test crashed. But I don't think you'd want to test for a crash occurring; that seems like a smell to me. If you can get your program into such a state in a realistic scenario, you should probably use error handling there?
did we discuss yet how crash would expose evaluation order, and whetehr/how we should deal with that?
e.g if I have
a = crash "foo"
b = crash "bar"
then according to our semantics the result is non-deterministic
Ayaz Hafiz said:
Probably
roc expectcan tell you when a test crashed. But I don't think you'd want to test for a crash occurring; that seems like a smell to me. If you can get your program into such a state in a realistic scenario, you should probably use error handling there?
Once you use crash in cases of "someone is using my API wrong", then that is something that should be tested IMHO.
Folkert de Vries said:
did we discuss yet how
crashwould expose evaluation order, and whetehr/how we should deal with that?e.g if I have
a = crash "foo" b = crash "bar"then according to our semantics the result is non-deterministic
I'm in the camp of "we should not deal with it" (but of course document that this indeed is non-deterministic).
the "ordering semantics" point instantly sold me on Ayaz's proposed restriction on where it can be used
it can't come up if you don't allow crash in defs in the first place!
I wonder if the rule could be simplified to "you can only use crash immediately after then, else, or ->"
Hm, it still could come up in the following case, no?
x1 = when y is
_ -> crash ""
x2 = when y is
_ -> crash ""
even if you say that it can only happen as the sole expression in a function, the problem remains:
crasher = \{} -> crash ""
x1 = crasher {}
x2 = crasher {}
hm, yeah true
:thinking: could we give a warning about it?
I guess this same problem already happens with integer overflow etc
If something is going to crash, do we really care about this? Either way it is a crash, the app state is broken, and the platform has do deal with it.
yeah maybe it doesn't matter
here's a draft PR for crash in the tutorial, to see what it would look like: https://github.com/roc-lang/roc/pull/4190
As soon as ordering is well-defined, people will start depending on this ordering and we will need to make the compilation process (esp. some of the optimization passes) more complex to ensure that this guarantee is always upheld.
I do not think that it is worth it.
I have to say that I really don't like crash! in packages!
Yes, we should have some construct like this for iterating purposes, but in the code that we would distribute, I don't know, at least then each package should have GLARING RED each place where they use escape hatches.
It makes me almost like a hole in a stomach to think that my Roc might have crashes sprinlked around.
Especially then they can be very non-informative crashes. Like "operation successfully failed, CRASH!"
UGA BUGA, but why!?!?
And to enforce nice crash culture i think would be really hard.
Is there a reason hitting a roc_panic doesn't stack-unwind today? (I couldn't find anything in zulip but I know im missing historical context)
No platform has implemented it? They just print and exit. Theoretically the platform would have to deal with it.
how would the platform implement it? Im pretty sure you would need Roc compiler support
Namely, I'm referring to making sure reference counts are appropriately decremented when a roc_panic happens. I guess you could do that by chasing through roc_alloc
so from what I've read, the general way to do this is using libunwind
which exposes C functions you can call to get a backtrace of the current call stack, including function names if the currently running executable has debug info (and function addresses if not)
which means it's something hosts can do, and there's an advantage to having it be done in the host which is that it means hosts can decide whether or not they want to pay for walking the current stack to assemble that info
Though even if a host walked the currently stack, they wouldn't really have the information to clean up.
Really the only way for a host to clean up currently is to use an arena allocator or some separate allocator for roc that they can dump when panic is called
right, that ended up being the design intentionally
I was thinking of just getting a stack trace to log a better error message, which a host should be able to do
basically if hosts want to clean up on panic, they need to implement roc_alloc (and other resources, e.g. file handles - which also can only be created by running host code) to write down a list of what needs cleaning up
and then on panic they can traverse that list and perform whatever cleanups are necessary
Okay so it sounds like the current design is to sweep through roc_alloc
doesn't have to be a sweep
like for example let's say you implement roc_alloc as a custom variation on malloc that's the same as normal malloc in every way except that it can give you pointers to all the pages it's allocated
Right, I just mean what you were saying - record somewhere the things that were allocated, and need to be freed eventually
ah, in that case, yes!
Even with libunwind it looks like you need some compiler support. At the very least DWARF info for some platforms. I don't really know much about this but it looks like you need to either use the C ABI everywhere for libunwind to do its thing (with DWARF info) or compile with unwinding directives, e.g. via llvm.
could very well be wrong though
these compiler things are all so magical
yeah that's correct
the compiler needs to output the necessary metadata for libunwind to do its thing, including both llvm and dev backends - although I think llvm might already do it automatically
I looked into this a couple of years ago, and I remember concluding this design would work, but I don't remember whether llvm did it automatically or not
well.. we emit almost no debug information today, even with llvm :sneezing:
I think you have to instruct llvm to do so explicitly. There are debug and unwind directives that it doesn't look like we inject today
also with the surgical linker things probably get trickier too
as I recall it's something along the lines of storing some metadata at the end (or maybe right before) each function
I suspect it'll be the same sort of thing as what we currently do for surgical linking, just with a slightly different format than ELF/Mach-O/COFF
yeah both of those make sense, re metadata and the linker. thinking from first principles you probably just need to know the address of the previous stack frame, instruction pointer, and size of the current stack frame. and maybe where to jump in the current stack frame to do your cleanup
Yeah, surgical linker will bash debug info and at a minimum lead to it being misaligned. That being said, it should be supportable, just hasn't been done.
I haven't tried it, but https://crates.io/crates/backtrace might be a quick way to try it out in the CLI platform
and the readme of https://crates.io/crates/mini-backtrace mentions some libunwind-llvm interactions
I understand the need to print out a stack trace. That makes sense.
But do we really need to decrement refcounts and free memory? Your process is going to terminate, so the OS will free all of its memory anyway.
depends on the platform - for a CLI app, totally! For a webserver, it probably wants to just respond with HTTP 500 for that request and keep handling the others as normal.
if an editor plugin crashes, it probably just wants to display an error and continue running all the other plugins as normal
Ah, OK. Yes that was an assumption I was making without realising it, thanks!
Last updated: Jun 16 2026 at 16:19 UTC