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.
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:
try
, my return should have to be a result, my return type not being a result shouldn't cause all my try
s to errorHaving 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 :)
Thanks for pointing these out! Let me try to respond after gathering my thoughts
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
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.
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
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 :)
@Sam Mohr ^
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
So, we're in a bit of a limbo between Tasks and purity-inference
! and ? in Task world desugar to Task.await thing \res -> ...
and Result.try result \ok -> ...
But try uses early return, which only works in purity-inference world because it returns Err err
, not Task.err err
So you need to only use try
in purity-inference world and ? in Task world
Until we get rid of Tasks, old !, and ?
And then we re-add ? as the proper try
version
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
That's... not great
All a bit messy right now :sweat_smile:
The ! and ? desugaring code is not resilient around purity-inference
So in short:
try
is currently confusing because it was implemented very simply, but we (or at least I) have more high-impact work to do first, and should prioritize that firstcool, 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
That may be true...
Let me look at that PR
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.
I feel like it has been a bad error message since ?
has been around... I don't know how to make it much better
So I think the best solution for now is to recommend against ?
and try
, which isn't satisfying
But will lead to a better user experience
I ran into a similar thing today and reopened this related issue.
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
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.
Sam Mohr said:
So I think the best solution for now is to recommend against
?
andtry
, 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.
"spooky action at a distance" doesn't sound very good lol
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.
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