The unification of the error union is not the same here; the new Try({}, [Exit(I32), ..]) behavior differs from the old Result {} [Exit I32]_. Do we currently have a way to achieve the old unification behavior? This is similar to the situation described in #9010.
@Jared Ramirez
can you provide a repro?
Here it is:
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
line : Str -> Try({}, [EmptyStrErr])
line = |str| {
if str.is_empty() {
Err(EmptyStrErr)
} else {
Ok({})
}
}
my_func : {} -> Try({}, [Exit(I32), ..])
my_func = |_| {
line("hey")?
Err(Exit(1))
}
main! = || {
match my_func({}) {
Ok({}) => Stdout.line!("Ok")
Err(_) => Stdout.line!("Err")
}
}
output:
❯ ./zig-out/bin/roc test/fx/repro.roc
-- TYPE MISMATCH ---------------------------------
This ? may return early with a type that doesn't match the function body:
┌─ test/fx/repro.roc:16:5
│
16 │ line("hey")?
│ ^^^^^^^^^^^^
On error, this would return:
Try({ }, [EmptyStrErr])
But the function body evaluates to:
Try({ }, [Exit(I32), ..])
Hint: The error types from all ? operators and the function body must be compatible since any of them could be the actual return value.
Found 1 error(s) and 0 warning(s) for test/fx/repro.roc.
app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" }
import cli.Stdout
line : Str -> Result {} [EmptyStrErr]
line = |str|
if Str.is_empty(str) then
Err(EmptyStrErr)
else
Ok({})
my_func : {} -> Result {} [Exit(I32)]_
my_func = |_|
line("hey")?
Err(Exit(1))
main! = |_args|
when my_func({}) is
Ok({}) -> Stdout.line!("Ok")
Err(_) -> Stdout.line!("Err")
output:
❯ ./roc main.roc
Err
I would love to hear your (initial) thoughts on this @Jared Ramirez, this is a blocker for the new basic-cli.
Been pretty swamped at work and I have not had the time to look into this yet. Hopefully for Friday/this weekend I should be a little more free to take a look
i believe the issue here is in the rust compiler, this type signature:
line : Str -> Result {} [EmptyStrErr]
is silently upgraded to:
line : Str -> Result {} [EmptyStrErr]_
which makes the Err payloads mergable when you call line("hey")?.
this is called "polarity", and it's not yet implemented in the new compiler. the good new for now though is that by this can be fixed by adding a .. to the type definition of line:
line : Str -> Try({}, [EmptyStrErr, ..])
this is semantically the same as what the rust compiler is doing, so it should unblock!
Amazing, thanks @Jared Ramirez :heart:
Last updated: Feb 20 2026 at 12:27 UTC