Stream: ideas

Topic: Basic cli error handling


view this post on Zulip Chris (Aug 26 2023 at 23:21):

Currently I am working on a script in roc and I find error handling in application code a little bit hard.
If I understand correctly right now if i wanted to log if an error happened I could either do:

when result is
    Ok a -> Ok a
    Err err ->
        {} <- Stdout.line "Error message" |> await
        Task.err err

or

err <- result
    |> Task.fromResult
    |> Task.onErr
{} <- Stderr.line "Error happened" |> await
Task.err err

I feel like both of those are a little bit wordy / unnecessarily complex, but could be abstracted into functions (maybe Task.logErr?). Also they work only if the function already returns Task, otherwise I would have to change the return type.

I wish there was something like rust's anyhow crate. Something like:

Error a : { msgs: [Str], source: a }

Error.withMsg : a, Str -> Error a
Error.withContext : Error a, Str -> Error a
Error.source : Error a -> a
Error.log : Error a -> Task {} *

(I'm not sure if this is correct roc code)
I will probably try to implement this, but wanted to ask first if I'm missing something about logging errors in roc or error handling in general.

view this post on Zulip Brendan Hansknecht (Aug 26 2023 at 23:51):

I think most apps fail a full pipeline and then handle final error and success cases together. So remove the error handling from the local place and move it to a more centralized decision location. Though also could make a helper to log if errors happen.

view this post on Zulip Brendan Hansknecht (Aug 26 2023 at 23:52):

Not a solution, just a comment/some context

view this post on Zulip Brendan Hansknecht (Aug 27 2023 at 00:02):

As a clarification it would still be that same first code sample, but only used once for a full pipeline of errors. Just match the exact error and create a message. Each pipeline stage can create very rich error types if needed.

view this post on Zulip Chris (Aug 27 2023 at 00:02):

But then a FileReadErr somewhere up the stack doesn't say much about what went wrong and where, so every function should wrap the error in some tag? And having a huge when .. is .. is not really good either probably?

view this post on Zulip Brendan Hansknecht (Aug 27 2023 at 00:05):

Sure, just need an error mapping function. Or make a wrapping error type. If a direct wrapping error type, just a short one liner to wrap |> Task.onErr WrappingErrType.

view this post on Zulip Brendan Hansknecht (Aug 27 2023 at 00:06):

Would actually be the same thing with an error conversion function, but you then also need to write the conversion function which is of course a full when is

view this post on Zulip Brendan Hansknecht (Aug 27 2023 at 00:08):

Of course you can use any error type you want and make an anyhow style error type. Personally I would rather use tags than strings and take advantage of that richness.

view this post on Zulip Chris (Aug 27 2023 at 00:09):

Thank you for help! I'll probably try both approaches and see what fits better

view this post on Zulip Brendan Hansknecht (Aug 27 2023 at 00:12):

Oh also, I don't recall the exact function name, but if you have a result and want to continue a task pipeline there should be something like Task.fromResult to get you back into task land.

view this post on Zulip Brendan Hansknecht (Aug 27 2023 at 00:12):

Then no when ... is, just a continued pipeline.

view this post on Zulip Luke Boswell (Aug 27 2023 at 12:35):

@ziutech we uave this example which is an attempt to explain somwle of these ideas. I would really appreciate your thoughts on this example and if you have any ideas to improve it.

view this post on Zulip Richard Feldman (Aug 27 2023 at 18:40):

yeah the way I personally like to do things like this is to refactor this:

when result is
    Ok a -> Ok a
    Err err ->
        {} <- Stdout.line "Error message" |> await
        Task.err err

...into:

result
|> Result.mapErr MyCustomErr

this will wrap the result's Err type in a MyCustomErr tag - so for example, if I had a Result Str (List Problems) then now I have a Result Str [MyCustomErr (List Problems)]

view this post on Zulip Richard Feldman (Aug 27 2023 at 18:41):

then later on, when I'm doing all my error handling in one big when, I can tell this apart from other, similar errors (e.g. FileReadErr) because of the MyCustomErr wrapper:

when result is
    Ok whatever -> ...
    Err (MyCustomErr list) -> ...

view this post on Zulip Richard Feldman (Aug 27 2023 at 18:42):

also worth noting: Task.fromResult is a handy way to get your Result into the same error-handling pathway as your tasks

view this post on Zulip Richard Feldman (Aug 27 2023 at 18:44):

something I'm doing with some code at work is that I have some URL parameters that have been split into a Dict, and then I do:

userId <-
    Dict.get params "userid"
    |> Result.mapErr UrlMissingUserId
    |> Task.fromResult
    |> Task.await

response <- # do a HTTP request here, then |> Task.await

so then later on, when I'm handling all the task errors that happened in that backpassing sequence, I also handl the Err UrlMissingUserId -> ... branch from the failed Dict.get right there too!

view this post on Zulip Richard Feldman (Aug 27 2023 at 18:45):

I've actually thought about adding a Task.awaitResult to combine Task.fromResult and Task.await, just to make it easier to use this approach :big_smile:

view this post on Zulip Anton (Aug 30 2023 at 12:30):

I've been thinking about error handling for scripts too, a large when does not feel ideal. You want to be able to easily see where something went wrong and have a stacktrace available. For scripts there is often no reason to keep running the program if an error occurs, so I was thinking about something like Rust's unwrap for both Result and Task. We could perhaps only allow this unwrap in app files. It would also help avoid all the {} <- ... |> await boilerplate like here.

view this post on Zulip Hannes (Aug 30 2023 at 14:31):

To be honest, I already write my own unwrap function in Roc that crashes when it receives an error, but I mostly use it for situations that I know are impossible but the type checker doesn't know about

view this post on Zulip Anton (Aug 30 2023 at 14:33):

It's currently impossible to unwrap a Task though right?

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:56):

if we did something like this, I'd want to call it like okOrCrash or something like that

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:56):

but it makes me nervous, to be honest

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:56):

it makes the path of least resistance be crashing

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:56):

and it does so without using the crash keyword

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:57):

what about this?

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:57):

|> Task.unwrapOr \err -> crash "I got this error \(err)"

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:58):

so it would be something like:

unwrapOr : Task ok err, (err -> ok) -> Task ok *

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:58):

and then the Result version could be:

Result.unwrapOr : Result ok err, (err -> ok) -> ok

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:59):

(maybe there's a better name for it)

view this post on Zulip Richard Feldman (Aug 30 2023 at 14:59):

but what I'd prefer about this is that you have to explicitly write out the crash, so it's really obvious in the code base where things might crash

view this post on Zulip Richard Feldman (Aug 30 2023 at 15:00):

in Rust it's always hard to spot .unwrap(), which bothers me about it - sometimes I'll do an unwrap() as a short-term thing, intending to come back later and turn it into graceful error handling, but then sometimes I forget and there's nothing in the code base reminding me

view this post on Zulip Anton (Aug 30 2023 at 15:18):

but it makes me nervous, to be honest
it makes the path of least resistance be crashing

Uhu I get it. If we go down this route it should definitely come with restrictions, like only allowing it in app modules.
I do think we need something to improve error handling for scripts.

view this post on Zulip Anton (Aug 30 2023 at 15:18):

I like the name okOrCrash.

view this post on Zulip Anton (Aug 30 2023 at 15:18):

For noticeability, I expect it should be possible to give it a color that stands out in the syntax highlighting of all popular roc editors.

view this post on Zulip Anton (Aug 30 2023 at 15:22):

Once we have a candidate we like, we should try re-writing this script with it to see how it looks/feels.

view this post on Zulip Richard Feldman (Aug 30 2023 at 16:33):

I'm pretty averse to the idea of having functions or keywords that only work in app modules. I think a pretty basic invariant that I don't really want to violate is that you can always take a chunk of your code and extract it into a different module (e.g. for code reuse), and restricting certain functions or keywords to only work in one type of module would break that :sweat_smile:

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 16:38):

I don't think we should add okOrCrash. I think we should leave that for user apps to define

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 16:38):

It is easy enough to write

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 16:39):

And generally should be avoided except in short scripts

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 16:41):

Also, I think the giant error when isn't necessarily bad as long as the app has no plans to recover. Anything that is recovered from probably should be handled more locally. That said, we should throw it in a function at the end of the file and not make it front and center. That is the main current problem for readability there.

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 16:43):

Also, if errors always are just gonna be printed and never recovered, I think Task Ok Str is fine.

I think we are mostly dealing with the complexities of not wanting to handle errors in smaller programs and scripts rather than something that would be a real problem as the language scales to larger codebases.

view this post on Zulip Richard Feldman (Aug 30 2023 at 17:26):

Brendan Hansknecht said:

Also, I think the giant error when isn't necessarily bad as long as the app has no plans to recover. Anything that is recovered from probably should be handled more locally. That said, we should throw it in a function at the end of the file and not make it front and center. That is the main current problem for readability there.

yeah I like that strategy, especially using _ in the type signature of that handlErr function so I don't have to list out all the errors it's receiving

view this post on Zulip Richard Feldman (Aug 30 2023 at 17:26):

Brendan Hansknecht said:

I think we are mostly dealing with the complexities of not wanting to handle errors in smaller programs and scripts rather than something that would be a real problem as the language scales to larger codebases.

honestly, why don't we make a separate platform for this? like roc-script or something

view this post on Zulip Richard Feldman (Aug 30 2023 at 17:27):

that's a basic-cli alternative except by default every I/O function returns a Task with * for its error type, and it just crashes with some reasonable error message if there's an error

view this post on Zulip Richard Feldman (Aug 30 2023 at 17:27):

and then maybe it offers both read and readChecked, with the latter being an opt-in to normal error handling if you want it sometimes, even if it's not the default

view this post on Zulip Richard Feldman (Aug 30 2023 at 17:27):

seems like an easy choice for Advent of Code

view this post on Zulip Anton (Aug 30 2023 at 17:36):

That seems to be a great solution :)

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 20:06):

Yeah, that all sounds great.

view this post on Zulip Luke Boswell (Aug 30 2023 at 22:30):

Could you just have an Unchecked module in basic-cli which exposes the effects with this signature and behaviour? All the same names etc just in an Unchecked sub-module?

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 23:57):

So you would use either Stdin.line or Unchecked.Stdin.line?

view this post on Zulip Brendan Hansknecht (Aug 30 2023 at 23:58):

If you only want unchecked stuff, you could alias it to the standard names?

view this post on Zulip Luke Boswell (Aug 31 2023 at 02:06):

Yeah alias it, or even import it e.g. Unchecked.Stdout.{ line }

view this post on Zulip Anton (Sep 01 2023 at 09:02):

Interesting! It would avoid a lot of duplication between roc-scripts and basic-cli, as well as all the extra CI stuff that would come with a new platform. This does seem to be the best solution.

view this post on Zulip Richard Feldman (Sep 01 2023 at 14:15):

hm, but that would make it pretty easy for a single project to mix and match...I don't think I'd want that if I'm building a CLI for work :sweat_smile:

view this post on Zulip Richard Feldman (Sep 01 2023 at 14:16):

for professional projects, I like proper error handling being the path of least resistance

view this post on Zulip Anton (Sep 01 2023 at 15:06):

makes sense!

view this post on Zulip Brendan Hansknecht (Sep 01 2023 at 15:49):

Not that it is implemented yet, but would the new version of effects that get passed around when initializing modules fix this.

In a corp app, you would only pass the checked versions from the root of the app down the chain. Access to unchecked would be impossible without editing the root app file, which could easily be written as not allowed in the code standards.

view this post on Zulip Brendan Hansknecht (Sep 01 2023 at 15:49):

Sounds like that could enable a single platform that still has very clear access control to the unchecked versions of wanted.

view this post on Zulip Anton (Sep 22 2023 at 10:11):

Supporting unwrapping of Tasks with the roc-script platform (not in general) seems like it has reasonable trade-offs. What do you all think?

view this post on Zulip Richard Feldman (Sep 22 2023 at 13:21):

personally I think what I really want there is that the default is that errors crash, and then I can opt into error handling with fooChecked versions of tasks.

I think if I have that I'm not sure when I'd want a Task.unwrap because I'd just not use the Checked version in the first place :big_smile:

view this post on Zulip Anton (Sep 22 2023 at 13:44):

Task.unwrap would mainly be to avoid lots of |> Task.await and backpassing, because they come with significant cognitive load.

view this post on Zulip Richard Feldman (Sep 22 2023 at 23:19):

:thinking: I think you still need the same amount of backpassing and |> Task.await even if there are no errors to handle, just to chain them together

view this post on Zulip Anton (Sep 23 2023 at 08:14):

Hmm, I'll try something out when I have time and report back


Last updated: Jun 16 2026 at 16:19 UTC