Stream: bugs

Topic: Extremely confusing errors from try and ? operator


view this post on Zulip Eli Dowling (Dec 01 2024 at 08:48):

I just spent half an hour trying to figure out how to use the try operator along the way I found a whole collection of confusing and unhelpful error messages. I'll document them here:
My first attempt:

parseLine=\x ->
    {before, after} = try x|>Str.splitFirst  " "
    a = try Str.toU64 before
    b =  try Str.toU64 after
    { a, b }

Produced there two errors:

── TYPE MISMATCH in examples/2020/01.roc ───────────────────────────────────────

This expression is used in an unexpected way:

25│      {before, after} = try x|>Str.splitFirst  " "
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^

This splitFirst call produces:

    Result {
        after : Str,
        before : Str,
    } [NotFound]

But you are trying to use it as:

    {
        after : *,
        before : *,
    }c


── TYPE MISMATCH in examples/2020/01.roc ───────────────────────────────────────

This return statement doesn't match the return type of its enclosing
function:

25│>      {before, after} = try x|>Str.splitFirst  " "
26│       a = try before|>Str.toU64
27│       b =  try after|>Str.toU64
28│       { a, b }

This returns a value of type:

    [Err *]

But I expected the function to have return type:

    {
        a : Result (Int Unsigned64) [InvalidNumStr],
        b : Result (Int Unsigned64) [InvalidNumStr],
    }

────────────────────────────────────────────────────────────────────────────────

These are both utterly useless.

  1. Confusing because I know it returns a result, but that's what try is supposed to handle.
  2. What the hell is [Err *]?? Why is that being returned? why is there no mention of my record? why are we expecting the values to be results? That makes no sense that wouldn't be compatible with toU64 (I have enough experience with roc to be able to answer some of these, but lets pretend I'm actually totally new to roc)

Eventually, after a fair bit of stuffing around I realised that try doesn't really behave the way I'd expect and is operating on the thing I'm piping in. This would make sense if try was a function, but given it's a keyword I'd expect it to operate on everything coming after it.

So eventually, by stuffing around for a bit, I end up here:

parseLine=\x ->
    {before, after} = try Str.splitFirst x  " "
    a = try Str.toU64 before
    b =  try Str.toU64 after
    { a, b }
── TYPE MISMATCH in examples/2020/01.roc ───────────────────────────────────────

This return statement doesn't match the return type of its enclosing
function:

25│>      {before, after} = try Str.splitFirst x  " "
26│       a = try Str.toU64 before
27│       b =  try Str.toU64 after
28│       { a, b }

This returns a value of type:

    [Err [NotFound]]

But I expected the function to have return type:

    {
        a : Int Unsigned64,
        b : Int Unsigned64,
    }

────────────────────────────────────────────────────────────────────────────────

At this point I understand that try is being treated as an early return here and so it's returning a result whereas the function is returning a value.
However:

Having not used roc for 6 months, I'm getting to encounter a whole bunch of things as a "new user" and I'm definitely going to try to make the most of it by identifying pain points as they come up :)

view this post on Zulip Sam Mohr (Dec 01 2024 at 08:52):

Thanks for pointing these out! Let me try to respond after gathering my thoughts

view this post on Zulip Sam Mohr (Dec 01 2024 at 08:53):

I think updating the tutorial will help a bit, but one of the bigger sources of confusion here comes from the current very simple implementation of the try keyword

view this post on Zulip Sam Mohr (Dec 01 2024 at 08:53):

I initially attempted to implement it as a proper keyword that did its own kind of type-checking, but I kept finding myself running into issues.

view this post on Zulip Sam Mohr (Dec 01 2024 at 08:55):

So as a stop-gap, we started with a simpler impl, where try is desugared to:

try myFunc myArgs

-- goes to
when myFunc myArgs is
    Ok val -> val
    Err err ->
        return Err err

view this post on Zulip Eli Dowling (Dec 01 2024 at 08:59):

Cool, that all makes sense, it's just got some gotchas right now.
Do you think it'd be with recommending the ? operator for now? given it's got less unexpected behaviour?

l I mentioned this in a PR @JanCVanB has for adding the try keyword and a few other things to the tutorial
https://github.com/roc-lang/roc/pull/7274
Hopefully a note there will help :)

view this post on Zulip Eli Dowling (Dec 01 2024 at 08:59):

@Sam Mohr ^

view this post on Zulip Sam Mohr (Dec 01 2024 at 08:59):

Though we eventually want to make a try-like operator work properly, with the advent of static dispatch and parens, we will probably just try to get ? to work properly first

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:00):

So, we're in a bit of a limbo between Tasks and purity-inference

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:00):

! and ? in Task world desugar to Task.await thing \res -> ... and Result.try result \ok -> ...

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:01):

But try uses early return, which only works in purity-inference world because it returns Err err, not Task.err err

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:01):

So you need to only use try in purity-inference world and ? in Task world

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:02):

Until we get rid of Tasks, old !, and ?

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:02):

And then we re-add ? as the proper try version

view this post on Zulip Eli Dowling (Dec 01 2024 at 09:02):

ahhh I see, I just tried to use the ? operator with a purity inference call and got a compiler crash. So that's a bit awkward to recommend

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:03):

That's... not great

view this post on Zulip Eli Dowling (Dec 01 2024 at 09:03):

All a bit messy right now :sweat_smile:

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:03):

The ! and ? desugaring code is not resilient around purity-inference

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:05):

So in short:

view this post on Zulip Eli Dowling (Dec 01 2024 at 09:07):

cool, well given the normal basic-cli is still using task. Maybe it's worth leaving try out of the tutorial until it's working a bit better. That way people doing AOC and trying roc now don't have to face that confusion

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:07):

That may be true...

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:07):

Let me look at that PR

view this post on Zulip Eli Dowling (Dec 01 2024 at 09:15):

Oh and I forgot one error. If you use the question mark operator you get this nonsense:

parseLine=\x ->
    {before, after} = Str.splitFirst? x  " "
    a = Str.toU64? before
    b =  Str.toU64? after
    { a, b }
── TYPE MISMATCH in examples/2020/01.roc ───────────────────────────────────────

This 2nd argument to this function has an unexpected type:

35│      b =  Str.toU64? after
              ^^^^^^^^^^^^^^^^

The argument is an anonymous function of type:

    Int Unsigned64 -> {
        a : *,
        b : Int Unsigned64,
    }

But this function needs its 2nd argument to be:

    Int Unsigned64 -> Result b [InvalidNumStr]

────────────────────────────────────────────────────────────────────────────────

To which my immediate thought is "but Str.toU64 only takes one argument..." and then "what is that function signature, I don't have a function matching that"

Again, it makes sense if you understand what's actually going on with the desugaring.... but that's not helpful for the punters.

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:17):

I feel like it has been a bad error message since ? has been around... I don't know how to make it much better

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:17):

So I think the best solution for now is to recommend against ? and try, which isn't satisfying

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:17):

But will lead to a better user experience

view this post on Zulip Dan G Knutson (Dec 01 2024 at 09:18):

I ran into a similar thing today and reopened this related issue.

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:19):

I've got a type bug to fix with early return, after which I can try better implementing try. I think we would just reuse the same impl if we move to postfix ? again

view this post on Zulip Eli Dowling (Dec 01 2024 at 09:19):

I agree.
It's probably not worth focusing on, we can try to sort it out with the new try keyword in the purity inference world and just give up on the Task version for now.

view this post on Zulip Eli Dowling (Dec 01 2024 at 09:22):

Sam Mohr said:

So I think the best solution for now is to recommend against ? and try, which isn't satisfying

Honestly, I kind of agree. The errors are just too much for most people to be able to comprehend and it's not something you can easily explain in the tutorial. The inference just gives you the "spooky action at a distance" feeling.

view this post on Zulip Sam Mohr (Dec 01 2024 at 09:23):

"spooky action at a distance" doesn't sound very good lol

view this post on Zulip Eli Dowling (Dec 01 2024 at 09:25):

hahah, no, but I think that's the best way to describe getting a cryptic error in the first call of my function that's caused by the lack of an Ok in the return value.

view this post on Zulip jan kili (Dec 01 2024 at 16:35):

Thanks for these insights, @Eli Dowling - I'll update my tutorial update to preview-ify the new try explanation and postview-ify the old ? explanation :yum:


Last updated: Jul 05 2025 at 12:14 UTC