I wrote this tiny program which doesn't typecheck:
main : Program
main = Program.noArgs (Task.attempt mainTask \result ->
when result is
Ok task -> task
Err _ -> Task.succeed {} |> Program.exit 1
)
mainTask =
Task.await
(File.readUtf8 (Path.fromStr "../../1"))
\contents ->
x = if contents == "asdf" then 1 else 2
Num.toStr x
|> Stdout.line
|> Program.exit 0
The issue here was that Program.exit
only accepts a Task {} [] fx
, but here clearly unhandled errors are present. Instead of this mismatch, I got two huge reports about await
and attempt
callbacks, which were hard to read (at least for me as a beginner).
Is passing a Task with unhandled errors to Program.exit
not a type mismatch? Or are there other reasons this is missing from compiler output?
This is what the compiler prints
if i understand correctly, in this case Program.exit
is not a type mismatch because it is only handling the good case. It will never get passed a Task
with unhandled errors. So then the issue becomes that the lambda has a type incompatible with the type of File.readUtf8
As for the second error. The Ok cases for matching on a result from Task.attempt
is not correct. The Ok
case does not contain a Task
, it just contains an ExitCode
. As such, it still needs to be passed to Task.succeed
To expand on the first error, Program.exit
has a type of Task {} [] fx, U8 -> Task ExitCode [] fx
. Which specifically says that it takes a Task
with no errors and returns a Task
that can never have errors. It is a closed tag union for the error ([]
vs []*
). This means that it is not allowed to expand/merge with another task to contain errors later.
The lambda if fine by itself because its input is a Str, Num.toStr
doesn't return any error types, Stdout.line
doesn't return any error types, and Program.exit
gets input a Task
with no possible errors.
Task.await
on the other hand takes the error case of the first argument and merges it with the ok case of the second argument. Since the error type of File.readUtf8
is [FileReadErr Path ReadErr, FileReadUtf8Err Path [BadUtf8 Utf8ByteProblem Nat]*]*
, it is trying to merge that with the error type of []
which is return from Program.exit
. This is impossible. A closed union []
can not be merged with anything. I guess Program.exit
was kinda written in a way that it expects to be terminal.
If you change the type signiture of Program.exit
to exit : Task {} []* fx, U8 -> Task ExitCode []* fx
, this should work.
I guess also to expand on the second error, the type of:
Task.attempt
is Task a b fx, (Result a b -> Task c d fx) -> Task c d fx
The function you wrote, took the Ok case to contain a Task
. That would mean you want a
to be Task ExitCode [] fx
. If you do substitution with that knowledge, you get:
Task (Task ExitCode [] fx) b fx, (Result (Task ExitCode [] fx) b -> Task c d fx) -> Task c d fx
But mainTask
is not return a nested Task (Task ExitCode ...)
. It is just returning a Task ExitCode ...
Hopefully this helps some.
That said, I guess it would be nice if we can make these error messages nicer somehow so this becomes clearer.
It should help, but I think it's too much to handle for me for now. I'll have to re-read this, the error messages and all the function signatures a few more times.
Thank you very much though!
Haha, no worries. I guess the summary is I am pretty sure this is working as expected, but the error messages aren't great because they are too large and not focused in on what really caused the bug.
With this program that doesn't have the nested task problem:
main : Program
main = Program.noArgs (Task.attempt mainTask \result ->
when result is
Ok exitCode -> Task.succeed exitCode
Err _ -> Task.succeed {} |> Program.exit 1
)
mainTask =
Task.await
(File.readUtf8 (Path.fromStr "../../1"))
\contents ->
x = if contents == "asdf" then 1 else 2
Num.toStr x
|> Stdout.line
|> Program.exit 0
I get:
── TYPE MISMATCH ──────────────────────────────────────────────────── 1/1a.roc ─
This 2nd argument to await has an unexpected type:
16│> \contents ->
17│> x = if contents == "asdf" then 1 else 2
18│>
19│> Num.toStr x
20│> |> Stdout.line
21│> |> Program.exit 0
The argument is an anonymous function of type:
Str -> Task ExitCode [] [Read [File]*, Write [Stdout]*]a ?
But await needs its 2nd argument to be:
Str -> Task ExitCode [FileReadErr Path.Path File.ReadErr,
FileReadUtf8Err Path.Path [BadUtf8 Utf8ByteProblem Nat]*]* [Read [File]*,
Write [Stdout]*]a ?
────────────────────────────────────────────────────────────────────────────────
1 error and 0 warnings found in 19 ms.
which I understand as: if errors are present in type of first Task.await
argument, they can't be thrown away in the callback
I still don't understand why the compiler doesn't complain when passing a task with errors to Program.exit
:thinking:
Oh, I'm passing the result of Stdout.line
, so it doesn't have errors anyway! So I don't know how errors get aligned in the callback if I don't use Program.exit
there
Probably because Stdout.line
has *
in place of errors?
I think the important part is to look at the core of Task.await
:
when result is
Ok a -> transform a
Err err -> Task.fail err
It takes a Task
as the first argument. If that Task
succeded, it passes its data through the lambda to get a new Task
. In this case, the lambda is named transform
. If the Task failed, it just returns the failed task. What is really important here is that the two cases are different branches of a when
expression. This means that they must have the exact same type.
The type of the Ok
case is Task ExitCode [] [Read [File]*, Write [Stdout]*]a
The type of the Err
case is Task * [FileReadErr Path.Path File.ReadErr, FileReadUtf8Err Path.Path [BadUtf8 Utf8ByteProblem Nat]*]* [Read [File]*, Write [Stdout]*]a
These two types are not compatible. Yes the difference is the *
. The Ok
case says it can never have any errors or merge with any Task
with errors by having an error type of []
. If the error type was []*
or just *
, it would work.
Task.await on the other hand takes the error case of the first argument and merges it with the ok case of the second argument
Reading the type signature it looks to me as if the error type needs to be the same:
await : Task a err fx, a -> Task b err fx -> Task b err fx
So I don't understand what you mean by "merging" but I think I get it now, thank you again!
Michał Łępicki has marked this topic as resolved.
Right, so by "merging" you probably meant that they need to be compatible, and if they are open tag unions, both sets of tags will be combined. All clear now I think!
Yeah, merging probably wasn't the best word. You got it.
Last updated: Jul 06 2025 at 12:14 UTC