Stream: ideas

Topic: Error handling for Stdin.line


view this post on Zulip Sebastian Fischer (Dec 16 2021 at 09:44):

The CLI-platform provides Task.attempt for handling errors in tasks and Stdin.line for reading lines from STDIN.

Currently, Stdin.line panics when insufficient input is given, as can be seen by running the following program.

app "attemptLine"
    packages { base: "cli-platform" }
    imports [
        base.Task.{ Task }, base.Stdout, base.Stdin
    ]
    provides [ main ] to base

main : Task {} *
main =
    _ <- Task.await (Stdout.line "attempting to read a line...")

    result <- Task.attempt Stdin.line
    when result is
        Ok input -> Stdout.line input
        Err _ -> Stdout.line "no input given"

When entering a line, it is echoed as expected. But when pressingCtrl-D (or when providing an empty file as input) this program panics:

thread '<unnamed>' panicked at 'called `Option::unwrap()` on a `None` value', src/lib.rs:116:45

My impression is that this is intended behavior, because the type of Stdin.line is Task Str * without an explicit error tag. Would it be possible/difficult to change the implementation of Stdin.line to return an Err result on insufficient input? Would that be a reasonable change to the CLI-platform? I think an Err result would be useful to read inputs from STDIN when the number of lines is not known in advance.

Another observation: The program above prints emitted runtime error VoidValue before rebuilding the host. The program runs as expected afterwards, but other programs (without Task.attempt) don't print this message.

view this post on Zulip Richard Feldman (Dec 16 2021 at 14:31):

it's easy enough to change the design, but I guess this gets into a question of how much the CLI example should be "production-focused" versus "beginner example" focused. I think we should definitely have an example with basic command-line input/output, where we don't worry about error handling yet because the goal would be to give an example of backpassing and chained tasks, not error handling yet

view this post on Zulip Richard Feldman (Dec 16 2021 at 14:32):

so maybe we should make a separate example for that in examples/ and then update the CLI example to handle stdin errors?

view this post on Zulip Richard Feldman (Dec 16 2021 at 14:32):

at that point I could also see an argument for having Stdout.line have an error type too, e.g. for "broken pipe"

view this post on Zulip Sebastian Fischer (Dec 16 2021 at 16:12):

I agree it's good to think about the focus for the examples.

I was motivated by using the CLI-platform for my own programs where I'd like to be able to read all of STDIN completely, regardless of how much input there is. But I understand that using the example platforms for different programs is not their primary purpose.

That being said, the platform for the CLI-example already provides Task.attempt for error handling, so it might be reasonable to include corresponding examples.

I agree that not all examples should be concerned with error handling. Could there be multiple programs (with and without error handling) in examples/cli? Then we could keep the current example Echo.roc as is, change Stdin.line (and possibly Stdout.line) to return custom errors, and add additional examples demonstrating how to handle them.

view this post on Zulip Sebastian Fischer (Dec 17 2021 at 09:35):

I now realize that my reasoning was based on the invalid assumption that CLI-programs don't need to handle all errors.

In Echo.roc the main function has the declared type Task {} * (with a wildcard for the error type). I assumed this type would allow Roc programs to provide main functions with failing tasks. (I am not yet sure I understand open tag unions completely, especially regarding which types can be unified with which.)

I tested my assumption with the following program.

app "failure"
    packages { pf: "platform" }
    imports [ pf.Task.{ Task }, pf.Stdout ]
    provides [ main ] to pf

main : Task {} *
main =
    _ <- Task.await (Stdout.line "about to fail...")

    Task.fail InsufficientInput

For this program roc check prints the following message:

Mismatch in compiler/unify/src/unify.rs Line 232 Column 13
trying to unify FunctionOrTagUnion(SubsIndex<roc_module::ident::TagName>(47), `#UserApp.1`, 110) with rigid var '*'

thread 'main' panicked at 'internal error: entered unreachable code: closure tags are internal only', reporting/src/report.rs:277:42

When changing the type of main to Task {} [InsufficientInput]* to include the error tag, roc check prints a longer error message:

Mismatch in compiler/unify/src/unify.rs Line 1071 Column 13
Trying to unify two flat types that are incompatible: [ Global('InsufficientInput') [], ]<149> ~ EmptyTagUnion

Mismatch in compiler/unify/src/unify.rs Line 859 Column 9
Problem with Tag Union
There should be 1 matching tags, but I only got
[]

── TYPE MISMATCH ───────────────────────────────────────────────────────────────
Something is off with the body of the mainForHost definition:

13│  mainForHost : Task {} [] as Fx
14│  mainForHost = main
                   ^^^^
This #UserApp.main value is a:

    [ Effect.Effect {} -> Result {} [ InsufficientInput ]a ]

But the type annotation on mainForHost says it should be:

    [ Effect.Effect {} -> Result {} [] ]
────────────────────────────────────────────────────────────────────────────────

So, I conclude that the platform requires the type of main to be (unifiable with) Task {} [], not allowing any unhandled errors in tasks.

As a result, my proposal to leave the existing echo example unchanged is not possible when changing Stdin.line to include an error tag. I'm not sure how to proceed - just wanted to share my reasoning and what I learned.

Two more questions:

1. Should the echo example be changed to declare main : Task {} [] with an empty error union?
2. Are the above error messages printed in the way they are intended to or should they be different? In what way?

I'd answer question 1 with a "yes", but am not sure how to answer question 2. It seems that at least the panic should be avoided, but if Task {} * can be unified with both Task {} [] and Task {} [InsufficientInput]* I don't see on what basis to reject the first version of the above program.

view this post on Zulip Erwin Kuhn (Dec 17 2021 at 11:45):

The error for roc check comes from the mismatch between the type of main and the type expected by the platform. That one is defined in platform/Package-Config.roc

If you also modify the package config to require main : Task {} [InsufficientInput]*, roc check is happy, but running the program causes a panic:

thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: UnresolvedTypeVar(102)', compiler/mono/src/ir.rs:2247:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Trying to guess, I think that Roc cannot handle a main function returning an open tag union? (which would make sense)

If you then modify the return type to Task {} [InsufficientInput] (without the *), the program runs successfully. Now, it would be up to the host to handle possible errors from the Roc application.

view this post on Zulip jan kili (Dec 17 2021 at 13:34):

@Erwin Kuhn Since the Roc team doesn't want to provide official platforms for use in production, it sounds like it's time for a (the first?) community-made/maintained platform for production CLIs!

view this post on Zulip jan kili (Dec 17 2021 at 13:38):

I can't make time to write a lot of code these days, but I'm happy to host the repo, unless you're interested in that.

view this post on Zulip Erwin Kuhn (Dec 17 2021 at 14:39):

Totally, it'd be great to see! So far, I started writing a tutorial on how to build platforms and should have a first (incomplete) draft this WE.

Maybe the goal of that tutorial could be to build up to the basics of CLI and I/O? And then we can complete that base into a full community platform?

view this post on Zulip jan kili (Dec 17 2021 at 14:43):

Perfect! I'll start thinking about features.

view this post on Zulip Sebastian Fischer (Dec 17 2021 at 15:09):

I think such a tutorial would be great. If the consensus is to not demonstrate error handling in the CLI examples, that would also be fine with me.

However, my remaining questions were concerned with the existing CLI-example platform (that does not allow unhandled errors) and how to use the type system to enforce that Roc programs must provide tasks that cannot fail. I'm still not sure how the type system can do that.

view this post on Zulip Erwin Kuhn (Dec 17 2021 at 15:52):

I think a full-featured CLI platform should return Result types to force users to deal with errors (for instance, if there's no more input). It's possible to do that with current platform hosts, let me give it a shot

view this post on Zulip Erwin Kuhn (Dec 17 2021 at 15:53):

However, I think you'll have to handle that error in the Roc program for now (which makes sense in the case of "no more input")

view this post on Zulip Erwin Kuhn (Dec 17 2021 at 15:55):

I'd be curious to know how platform hosts can receive return values (including errors) from the main function of a Roc application though!

It may not be best practice for errors (better to handle them within the app if possible), but could be interesting in the general case!

view this post on Zulip Sebastian Fischer (Dec 17 2021 at 15:56):

I agree, and I think it would be possible with the task API from the example platform which gives access to result types via the attempt function.

view this post on Zulip Brendan Hansknecht (Dec 17 2021 at 16:22):

I would definitely be willing to help make a closer to production grade cli example. I think most of the changes will be around using tag unions for error cases in effects and deciding on certain API changes.

I would guess that this will most be changed to the effect functions. I don't think that it will necessarily make sense for main to return an error to the platform. Unlike, for example, an http server platform where the host can do something with errors(return 400 etc), i don't think that the cli platform would know what to do with the error. Best case it would probably print the error and exit (which could already be done in main). But totally possible that i am missing something.

view this post on Zulip Brendan Hansknecht (Dec 17 2021 at 16:24):

Also, the platform can receive errors from roc, the important part is that the error union is decided by the platform and would not be flexible. So a more defined API. They should just be tag unions

view this post on Zulip Erwin Kuhn (Dec 17 2021 at 16:26):

The current version of getLinein the host (platform/src/lib.rs) is:

#[no_mangle]
pub extern "C" fn roc_fx_getLine() -> RocStr {
    use std::io::{self, BufRead};

    let stdin = io::stdin();
    let line1 = stdin.lock().lines().next().unwrap().unwrap();

    RocStr::from_slice(line1.as_bytes())
}

The problem here is the .unwrap().unwrap() which panics. Instead, we want to return a Result (well, RocResult, due to memory layout). Here's a possibility:

#[no_mangle]
pub extern "C" fn roc_fx_getLine() -> RocResult<RocStr, ()> {
    use std::io::{self, BufRead};

    let stdin = io::stdin();
    let mut lines = stdin.lock().lines();
    if let Some(Ok(text)) = lines.next() {
        return RocResult::Ok(RocStr::from_slice(text.as_bytes()));
    }
    return RocResult::Err(());
}

Then you'd have to go and change the Package-Config.roc to reflect the return type: getLine : Effect (Result Str {})
Then you need to change Stdin.line and use that in the Roc program instead

I'm indeed struggling a bit the tag union on the error case for Stdin.line, I'll have to get back to it later. If someone gives it a shot in the meantime, would love to see the result :smiley:


Last updated: Jun 16 2026 at 16:19 UTC