Luke Boswell said:
main = run |> Task.onErr \err -> Stdout.line "ERROR: $(Inspect.toStr err)"
This is all great. One thing I would like to add (please take no offense) is that in my humble opinion, we should not encourage anyone to handle errors in a way that makes an application return "successfully" with exit code 0 (Stdout.line ...
will do just that). It is very problematic when scripts crash with exit code 0.
It is like what I saw (decades ago) with Java tutorials full of examples main
functions with try ... catch (...) {}
just to get rid of a checked exception problem, when it was SO EASY to just add a throws ...
to the function declaration and it wouldn't poison minds of those poor souls hoping for a better future with OOP: Java edition :cry:
Good point. How would you write the error handling for this example?
Perhaps something like this? (updated)
main =
Task.onErr run \err ->
Stderr.line! "ERROR: $(Inspect.toStr err)"
# non zero exit code
Task.err 1
Probably stderr as well
Maybe it would be helpful to have a helper in basci-cli
?
# helper defined in the platform
crashOnErr : Task {} a -> Task {} I32
# becomes
main = crashOnErr run
Luke Boswell said:
Good point. How would you write the error handling for this example?
Maybe with a crash
https://www.roc-lang.org/tutorial#crashing (I haven't use it yet).
Crash isn't the right option for error handling. It is more of a full abort and stack trace dump.
Really only meant for impossible situations or quick scripts.
For real error handling it is definitely a proper error message and exit code that would be wanted for basic CLI.
8 messages were moved here from #beginners > Understanding tasks by Brendan Hansknecht.
Brendan Hansknecht said:
Crash isn't the right option for error handling. It is more of a full abort and stack trace dump.
Bigger scripts would benefit from something similar, typically it's handled via function called die
. It does not display stacktrace, but aborts.
My preference is to have all relevant helpers/functions to return their error, but placing multiple (conditional) calls to die
in the main function/glue code.
I mean you can make a function called die
that prints a message to stderr and then returns an error code to the platform. That would exit with the error code.
I've been using crash
in my Roc code to signify assertion-failure-like (but to the extreme) execution paths. In other words, if we hit this execution flow, it means something has gone (horribly) wrong unexpectedly with (usually) the upstream logic and/or the local scope; and at the same time our test coverage complements this with a sufficient level of business logic tests.
Based on the comments in this thread, it sounds like this isn't probably idiomatic enough.
Would it be preferable instead to use inline expect
statements and propagate an unexpected error of sorts and use that to write to stderr eventually as we return the error code to the platform?
I think it is very important to split into two categories:
For 1, please crash
whenever you feel like it. It partially made to get out quick when you don't care about handling errors.
For 2, crash
should preferably only be used for situations you expect to be unreachable. The rest of the time, proper errors should probably bubble up to the caller. An extra note, crash is fine if a dev is simply using your API wrong and you want to fail them earlier, but it is better to design apis to avoid that if possible.
expect
is no different from crash really, but it can make sense to use as the early fail for API misuse.
Like in the docs say, "this API does not work with 0 inputs". The start the function with expect input != 0
.
But if you expect dynamic inputs and this might contain zero, return a proper result with an error is better.
All tradeoffs. Not really sure if there is really a specific style guide here yet.
I think expect
gets optmised out in release builds. So it's only helpful for roc dev
and roc test
.
It's like a debug_assert
Long term (medium?), I think expect will be allowed in more cases (definitely in non-optimized builds even without roc dev
). But it will always be opt in for optimized builds.
Just requires some wiring to detangle it from our memory buffer stuff and give the platform control.
oh I actually always intended for expect
to work like debug_assert
!
I think it's really important for there not to be a runtime perf downside to it, so you can verify absolutely any assumptions you have that you think should be true at that point (but can't reasonably verify using the type system) without having to think about what those checks will do to your release perf :big_smile:
Yeah. So they will be in all non-optimized builds. Including simple roc build app.roc
. so that is beyond roc dev
. They will not be in roc build --optimize app.roc
. Though we may add flags to turn on those features with optimized builds: roc build --optimize --with-expects --with-dbg app.roc
. just to give users extra info and expects as an opt in if they want.
That's at least what I thought the overall plan was
ah I see
my default thinking is that we shouldn't do that for expect
, similar to how Rust hasn't needed to do that for debug_assert
it really messes with the incentives if you know it might be used in optimized builds
and I can't really think of a scenario where you're doing local development, you need an optimized build for perf, and you also want expects for behavior
I assumed that was cause it has a separate assert
, but I see what you mean. Especially if it is either 100% on or 100% off. You probably would need to be selective by module if we decided to add a flag for that.
yeah exactly
assert
is basically a convenience wrapper around panic
, so crash
has that use case covered if bailing out genuinely seems like the best thing to do
and I can't really think of a scenario where you're doing local development, you need an optimized build for perf, and you also want expects for behavior
Roc has some pretty big optimization that if not applied can lead to such terrible perf or memory that apps aren't usable sometimes. Hopefully we can make debug optimize just enough that we don't have that issue, but in a number of languages, my preferred development config is release with debug related features. Cause sometimes perf is needed for the app to be usable, but I still want all debug features possible.
But I totally also think that my comment above is just noting a common failure case. If I didn't hit it, I would just live in dev 100% of the time for local development.
yeah that's fair
I kinda wonder if --optimize
and --release
should both exist :thinking:
I'm not sure what other distinctions would exist besides debug info though haha
In my mind, it can be nice to have 3 modes:
Brendan Hansknecht said:
I think it is very important to split into two categories:
- larger projects, libraries, anything that wants to be very robust
...
For 2,
crash
should preferably only be used for situations you expect to be unreachable. The rest of the time, proper errors should probably bubble up to the caller. An extra note, crash is fine if a dev is simply using your API wrong and you want to fail them earlier, but it is better to design apis to avoid that if possible.
That's what I meant as well, yes. Thank you! :pray:
Last updated: Jul 05 2025 at 12:14 UTC