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.
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.
Not a solution, just a comment/some context
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.
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?
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.
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
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.
Thank you for help! I'll probably try both approaches and see what fits better
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.
Then no when ... is, just a continued pipeline.
@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.
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)]
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) -> ...
also worth noting: Task.fromResult is a handy way to get your Result into the same error-handling pathway as your tasks
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!
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:
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.
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
It's currently impossible to unwrap a Task though right?
if we did something like this, I'd want to call it like okOrCrash or something like that
but it makes me nervous, to be honest
it makes the path of least resistance be crashing
and it does so without using the crash keyword
what about this?
|> Task.unwrapOr \err -> crash "I got this error \(err)"
so it would be something like:
unwrapOr : Task ok err, (err -> ok) -> Task ok *
and then the Result version could be:
Result.unwrapOr : Result ok err, (err -> ok) -> ok
(maybe there's a better name for it)
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
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
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.
I like the name okOrCrash.
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.
Once we have a candidate we like, we should try re-writing this script with it to see how it looks/feels.
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:
I don't think we should add okOrCrash. I think we should leave that for user apps to define
It is easy enough to write
And generally should be avoided except in short scripts
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.
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.
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
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
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
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
seems like an easy choice for Advent of Code
That seems to be a great solution :)
Yeah, that all sounds great.
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?
So you would use either Stdin.line or Unchecked.Stdin.line?
If you only want unchecked stuff, you could alias it to the standard names?
Yeah alias it, or even import it e.g. Unchecked.Stdout.{ line }
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.
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:
for professional projects, I like proper error handling being the path of least resistance
makes sense!
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.
Sounds like that could enable a single platform that still has very clear access control to the unchecked versions of wanted.
Supporting unwrapping of Tasks with the roc-script platform (not in general) seems like it has reasonable trade-offs. What do you all think?
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:
Task.unwrap would mainly be to avoid lots of |> Task.await and backpassing, because they come with significant cognitive load.
: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
Hmm, I'll try something out when I have time and report back
Last updated: Jun 16 2026 at 16:19 UTC