Stream: beginners

Topic: Error handling with tasks


view this post on Zulip witoldsz (Apr 10 2024 at 00:22):

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:

view this post on Zulip Luke Boswell (Apr 10 2024 at 00:26):

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

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 00:26):

Probably stderr as well

view this post on Zulip Luke Boswell (Apr 10 2024 at 00:31):

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

view this post on Zulip witoldsz (Apr 10 2024 at 00:34):

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

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 00:42):

Crash isn't the right option for error handling. It is more of a full abort and stack trace dump.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 00:43):

Really only meant for impossible situations or quick scripts.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 00:43):

For real error handling it is definitely a proper error message and exit code that would be wanted for basic CLI.

view this post on Zulip Notification Bot (Apr 10 2024 at 00:45):

8 messages were moved here from #beginners > Understanding tasks by Brendan Hansknecht.

view this post on Zulip Przemek Kitszel (Apr 10 2024 at 08:58):

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.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 14:26):

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.

view this post on Zulip Hristo (Apr 10 2024 at 20:53):

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?

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 21:24):

I think it is very important to split into two categories:

  1. Small hobby and quick scripts
  2. larger projects, libraries, anything that wants to be very robust

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.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 21:26):

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.

view this post on Zulip Brendan Hansknecht (Apr 10 2024 at 21:27):

All tradeoffs. Not really sure if there is really a specific style guide here yet.

view this post on Zulip Luke Boswell (Apr 11 2024 at 00:44):

I think expect gets optmised out in release builds. So it's only helpful for roc dev and roc test.

view this post on Zulip Luke Boswell (Apr 11 2024 at 00:48):

It's like a debug_assert

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 00:53):

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.

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 00:53):

Just requires some wiring to detangle it from our memory buffer stuff and give the platform control.

view this post on Zulip Richard Feldman (Apr 11 2024 at 01:46):

oh I actually always intended for expect to work like debug_assert!

view this post on Zulip Richard Feldman (Apr 11 2024 at 01:47):

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:

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 01:54):

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.

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 01:55):

That's at least what I thought the overall plan was

view this post on Zulip Richard Feldman (Apr 11 2024 at 01:58):

ah I see

view this post on Zulip Richard Feldman (Apr 11 2024 at 01:59):

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

view this post on Zulip Richard Feldman (Apr 11 2024 at 01:59):

it really messes with the incentives if you know it might be used in optimized builds

view this post on Zulip Richard Feldman (Apr 11 2024 at 02:00):

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

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 02:00):

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.

view this post on Zulip Richard Feldman (Apr 11 2024 at 02:01):

yeah exactly

view this post on Zulip Richard Feldman (Apr 11 2024 at 02:02):

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

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 02:03):

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.

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 02:05):

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.

view this post on Zulip Richard Feldman (Apr 11 2024 at 02:05):

yeah that's fair

view this post on Zulip Richard Feldman (Apr 11 2024 at 02:06):

I kinda wonder if --optimize and --release should both exist :thinking:

view this post on Zulip Richard Feldman (Apr 11 2024 at 02:06):

I'm not sure what other distinctions would exist besides debug info though haha

view this post on Zulip Brendan Hansknecht (Apr 11 2024 at 02:08):

In my mind, it can be nice to have 3 modes:

  1. full dev (fast build, minimal optimization, debug info, debug statements, expects)
  2. low opt (ok speed build, the core optimizations, debug info, debug statements, expects)
  3. full opt (slow build, max optimizations)

view this post on Zulip Hristo (Apr 11 2024 at 05:15):

Brendan Hansknecht said:

I think it is very important to split into two categories:

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