Hey all! I wrote a proposal about how we can concisely add context for errors alongside the try keyword. Please take a look and let me know what you think!
https://docs.google.com/document/d/1pBNytZYF5aOCYgmHno2Y-8im-tWfbLLYN5RIu8dEe6s/edit?usp=sharing
I like this. I Always felt a bit awkward with mapErr, even though it is perfectly sensible
Yeah seems nice. Not sure if we can pick a better wording than else....but this seems the right way to go overall. We want to promote wrapping errors and adding context. It is a great way to handle errors. So we want that to be minimally verbose
Dare I say it… what about catch instead of else? Would be familiar to many, and to me at least it reads well with a lambda:
try action catch \error -> …
Though I guess handle, as you suggest I think, is also quite nice
Man, I spent SO much time trying to think of a better version than else.
catch was one of the options, which makes sense when you pass an inline error mapper that "catches and handles" your error, but I think it's visually confusing for the "wrap with context" case a literal tag name gives
Almost feels like we should just commit to try catch, but:
Of the options, I like else the best so far.
Good points.
I like else as well. It does read well.
I like this!
Is else required if you're not wrapping errors?
jsonData =
try File.read! "some-file.txt"
|> try Json.decode
if that's allowed, then are there any syntax ambiguities if you use this in an if branch? (Will the parser be able to tell whether an else is for the try or the next if branch?)
Can you pipe a result into try?
jsonData =
File.read! "some-file.txt"
|> try else FileReadErr
|> Json.decode
|> try else JsonDecodeErr
Sky Rose said:
I like this!
Is
elserequired if you're not wrapping errors?jsonData = try File.read! "some-file.txt" |> try Json.decodeif that's allowed, then are there any syntax ambiguities if you use this in an
ifbranch? (Will the parser be able to tell whether anelseis for thetryor the nextifbranch?)
Else is not required, and the parser will consider else part of the try expression, it's pretty well structured.
I didn't consider the try else piping syntax... I like how it reads! Since it means the operation we're trying is no longer preceded by try, I think we lose the try thing sentiment, just a bit
But I'd like to hear what others think! For now, even though it's maybe less visually aligned, the try else with a newline in the middle stays as one statement, which I prefer
Piping to try makes some sense, but piping to try else seems really strange.
I think one oddity of piping to try is that it feels like it is just a regular function. So quite weird that it magically returns. With try on the same line as the called function, it at least feels like a proper keyword
Yeah, probably shouldn't support piping to it, then
I think piping to dbg makes sense for that reason, it returns the value it logged. try doesn't quite do that. I think it's always more clear when try is on the same line
I actually have never thought piping to bare try looked good. Would people be okay with not allowing that? I think it's very valuable to keep it on the same line as the fallible expression
I think I would like piping. I think of it as two steps. First make the result, then unwrap it. That makes it possible to do something with the Result in between making it and unwrapping, like logging it or transforming it with a function that takes a Result.
I like that the pipe makes try feel like a function. It's got an input and an output. The magical return in the middle is only slightly weirder than a function that does effects in the middle of a pipeline, which is allowed.
a downside of else I realized is that it's coupled to try, whereas alternatives (such as an infix operator like ?>) can work standalone
e.g. in the recent discussions about tracing spans, this can work:
fileContents =
try Trace.span context \{} ->
File.read! "some-file.txt" ?> ReadFailed
...but else in the same place can't work because there's no try in that lambda
(I'm not saying ?> is the way to go, just that an ordinary infix operator doesn't have this downside)
I also think it's a bit confusing that else usually means "the thing following else is a value" but in this context it means "the thing following else is a function that gets applied"
and one consequence of that in conjunction with tags being either values or functions is that I don't think a beginner who's looking at a lot of try foo else Bar lines would realize that Bar is a function
it also occurred to me that we could just use ? for this:
fileContents =
try Trace.span context \{} ->
File.read! "some-file.txt" ? ReadFailed
I don't think people have as strong expectations for what ? would mean, because in this design it wouldn't appear anywhere else in the language
Part of the reason I suggested else in particular was because it was more generic than catch, and that seemed to be a good benefit. I think you're right, something with ? has the same benefit of not having a specific meaning already
I'd prefer ? because it doesn't have > after it, meaning it's not a piping operator
And that means we'd use it inline instead of on the next line
(for the most part)
I think I'd personally be okay with ? instead of else even if it's less "it reads in English" because it's weird on purpose as to need users to look it up anyway, and it allows usage without try
Thinking through everything in the proposal including desugaring and syntax, I think I'd want everything to work the same
yeah the binding rules seem to fit together nicely!
there's a related question of whether we want an operator for |> Result.withDefault
like some languages use ?: or ?? for that
The TypeScript one is ??, Python/Ruby is or. I'd vote ?? for consistency if we go for ?
Not sure if we need one, though
Swift uses ??
(that's where I heard about it first)
This else syntax eats the cost of being terse and confusing to make the cost as low as possible to add context for error tracing. I don't think Result.withDefault has the same needs
I think it's fun that ?: is known as the "Elvis operator" but I have to admit I always have to use that nickname to remember whether it's :? or ?:
See other threads in this topic if you want to show that people like fun syntaxes
yeah I agree that it's not as important, but I've missed it compared to languages that have it
The thing I want is a heuristic we can use to say "this is too many operators!"
I don't have that, so I feel unsure
But it's very useful, yes
:thinking: I wonder if we should actually make that consistent with how default record fields work, e.g.
fn = \{ foo ?? 0, bar ?? 1 } ->
it's almost literally "with default"
hm, although actually that could be confusing
in that it might suggest foo is a Result and you're defaulting away the Result in the pattern or something :thinking:
No, I like that!
It feels like slightly confusing is better than confusing
? now means mapErr
right
So ?? is closer to "default value"
should the type change too for consistency? :thinking:
{ foo ?? Str, bar ?? Str }
I guess that kinda looks like "foo's type defaults to Str" :sweat_smile:
I'd say so
For the same reason as above
Unless we change for a different operator for mapErr
Actually, we could just swap these two
? now means default value, ?? means mapErr
That preserves the current meaning of ?, meaning fewer changes
Though I think I'd prefer ?? or ?: for consistency with other languages
There's so many layer to consider. It's fun to see the discussion. I like the latest evolution using ? and ?? :smiley:
I guess it's hard until we step back and put it into a proposal and work through all the code examples and edge cases we've built up before we can know if it works.
Sure, I can make a v2 with the new operators and more examples
despite what I said earlier against ?: - there is actually something interesting about how : appears in records and types:
{ foo : Str, bar ?: Str }
fn = \{ bar ?: "" } ->
It kind of implies "assign if necessary"
Which is what we want
that said, I still prefer ?? :laughing:
I'll go with ?? for now, I agree
Actually, issue: try Utc.now! ? FailedToGetTime
We're pretty close to !?, that's what else avoids
I don't have a problem with it if there's a space
Ok, sure
especially with syntax highlighting:
try Utc.now! ? FailedToGetTime
there's an argument for ? should be withDefault and something else should be mapErr
namely that withDefault should be consistent with how it works in records
and that ? looks better than ?? in record types
I think ?? could be fine for mapErr, but it would probably be surprising to people coming from Swift and TS, who would assume that would be withDefault
Yeah, and I don't know another language that uses ? for default values in records
So I'll propose we change default records to use ??
maybe it's better to have it be ? in the type and ?? in the destructure :thinking:
like { foo ? Str, bar ? Str } and then fn = \{ foo ?? "", bar ?? "" } ->
It should at least be in the destructure IMO
I'll have to think on it, this system in its entirety isn't "falling into place" in my head, comfort-wise
yeah
certainly using ? as its current uses plus "with default" seems like a reasonable idea
it just means finding another one for mapErr
Yep
We aren't using @ or $ really at the moment
Those are your easy to type operators because they're number shifts
@ is for opaque types
So maybe $ works? It's already an arbitrary operator. I'd vote $ or a ? plus something else
I could see doing ?? for mapErr and just explaining that it's kinda swapped
but maybe that's easy for me to say when I'm not already used to it :laughing:
Sure, though at that point, are we just swapping so that we can preserve default values in records?
What's the reason to swap
yeah exactly
well not just for the sake of not changing it, but rather because I think { foo ? Str } reads better than { foo ?? Str }
maybe I'd get used to it though
it doesn't come up that often, to be fair
I always prefer consistency and orthogonality
So I think it's worth swallowing that bullet
Python, Javascript, Typescript, Kotlin, Swift, Ruby, C++, ..., all use = to set a default value for an argument. Is this out of the question? I feel like it would be one less thing to learn:
f = \x, {y, z=123, w=1.5, g} ->
...
And perhaps the type annotation could look like this:
f : Dec, {y: Dec, z: U64?, w: Dec?, g: I64} ->
It feels natural to me this way, but I haven't thought of the possible complications with the rest of the syntax.
Or perhaps = for the default value, and ?: for the type annotation:
f : Dec, {y: Dec, z?: U64, w?: Dec, g: I64} -> I64
f = \x, {y, z=123, w=1.5, g} ->
...
I quite like how this reads. I don't think of it as a ?: operator, I would even allow spaces between ? and : (but the formatter could get rid of them):
f : Dec, {y : Dec, z? : U64, w? : Dec, g : I64} -> I64
I'm not sure we would allow spaces between the field name and the ? however.
@Sky Rose
I like that the pipe makes
tryfeel like a function. It's got an input and an output. The magical return in the middle is only slightly weirder than a function that does effects in the middle of a pipeline, which is allowed.
I'm not as much of a fan, but since we're leaning towards a general mapErr operator with ?, I'm less worried about |> try else FileReadErr having try else. Maybe we can discuss whether we should allow piping to try in the #ideas>`try` keyword instead of `?` suffix topic?
@Aurélien Geron putting ? after the type now runs into the same suffixing problems we had before with type applications; what do we do for Dict? Str Str, or maybe Dict Str Str??
That's why I'd be open to putting it after the field name. We're actually already going to do that for any field that represents an effectful function, so I think this looks pretty consistently-styled:
Recorder a : {
seed : U64,
record! : a => {},
destFile? : Str,
}
We currently format all fields to have a space before and after the colon, I think that's still a clean formatting, so I'd not be a fan of sticking the ? onto the :
With respect to = for default values in a struct, I'd be okay with it, but would rather use a single operator to mean "default value", which is why I'd lean towards ??
As we're already planning on aliasing it to Result.withDefault in expressions
Ah if ?? is going to be used for Result.withDefault then I guess it makes sense to use it for default args. I quite like the {a, b? : U64, c! : Foo} syntax.
That's alignment! Tonight I'll make the v2 doc so we can all look over some code examples
Here's the second version of the doc! https://docs.google.com/document/d/1rb2gZNKKAwya2JQQyV9Tsjj_8K1p-P_-DgBXCI0WqOY/edit?usp=sharing
Since there are no new ideas in there, I'm avoiding making a new thread
Please review, and let me know if there's anything you disagree with! It seems like we're good to go on everything except for probably the "default record field type" annotation.
I suggest in the doc that we should use field? : Type, but that gets an interrobang when we have defaultable fields that hold an effectful function. e.g.
Button a : {
onClick!? : ClickHandler a,
}
So we should probably keep the current syntax for that, e.g. field ? Type
Otherwise, I kept the decisions from this thread in the doc:
Result.mapErrResult.withDefaultI'll make some GitHub issues once we've agreed that v2 looks good
Thanks for setting up the doc @Sam Mohr, can you enable public access for the doc? I'm currently getting access denied
@Anton should be fixed
Working for me :thank_you:
Though now I can edit it also @Sam Mohr -- which may be because I requested access and you approved that. I just assumed it was from your sharing above. Can someone else confirm if they can edit this??
Would it ever make sense to use both ? and ?? in the same expression. maybe this should be a warning?
Here the FileReadErr is never used as we are providing a Result.withDefault
jsonData =
try File.read! "some-file.txt" ? FileReadErr ?? "default contents"
Can we currently have default values for functions onHover ? HoverEvent -> Update a, I don't think I've seen this before.
## Create a new turtle at the origin facing right with the pen down.
new : { position? : Position, direction? : F64, pen? : [Up, Down] } -> Turtle
new = \{ position ?? { x: 0, y: 0 }, direction ?? 0, pen ?? Down } ->
@Turtle { position, direction, pen, lines: { current: [{ x: 0, y: 0 }], previous: [] } }
This is inconsistent with ! in names. We named the field position?, but it is used as only position. I am really not a fan of this inconsistency.
It will make it feel like a position! should be used as position, which is incorrect
Agreed
So I vote for keeping the syntax as it is right now
Should it be changed to double ? In the type? ??
Just to match the defaulting?
41 messages were moved from this topic to #ideas > alternatives to try for errors by Brendan Hansknecht.
Just to keep this discussion focused on try specifically.
Brendan Hansknecht said:
Should it be changed to double ? In the type?
??
I suggested this in the doc as an alternative, I'm down but people weren't as big on the look compared to alternatives/the current syntax.
Also, ?? means "extract or default" at the moment, so ?? also meaning "type of defaultable field" would be close but different
(this was moved with the blob)
Sam Mohr said:
So I vote for keeping the syntax as it is right now
Do you mean no try and no ?? or something more specific?
@Anton I only mean using ? to mean "type of defaultable field", all other proposals in v2 would be kept.
Meaning I want the below:
# preferred
Button a : {
onClick ? Handler a,
}
# also okay
Button a : {
onClick ?? Handler a,
}
# keep the following proposed changes
fileContent = try File.read! "file.txt" ? FileReadErr
confileFile = args.configPath ?? "~/.config/service.json"
List.range = { start, end, step ?? 0 } -> ...
Luke Boswell said:
Would it ever make sense to use both
?and??in the same expression. maybe this should be a warning?Here the
FileReadErris never used as we are providing aResult.withDefaultjsonData = try File.read! "some-file.txt" ? FileReadErr ?? "default contents"
You're right, even though we "use" the values, if there's a pure mapErr being used, it discards the error anyway. We should consider adding a warning for pure ? -> ??, but not if the mapErr is effectful, as it may be used to log something (a useful op).
Also, you are the only one with edit access
If we only allow ? to be used with try it would be impossible to use it with ?? cause try will propagate the error case.
Well, except for nested Results. Always a gross case to support, but it'll happen
Sure, but normally a type error and when you see it, it means it has to be a nested result. Sounds fine
I realize I missed something in your comment
We changed to ? over else so we could use it without try
Say
Richard Feldman said:
it also occurred to me that we could just use
?for this:fileContents = try Trace.span context \{} -> File.read! "some-file.txt" ? ReadFailed
This
That needs to be preserved IMO
If we dislike that, I think we should go back to else
Vs:
fileContents =
try Trace.span context \{} ->
try File.read! "some-file.txt" ? ReadFailed
Doesn't seem like a big deal to me. The try would be needed there if you had multiple ops anyway
but sometimes I might want to use ? without using try in a position where try would short-circuit
like I want ? but I don't want short-circuiting, and if I have to use try to use ? then I have to opt into short-circuiting where I don't want it
We always want to run effects in the current system, which is why ! is now glued onto names
Not true of results IMO, for the reasons Richard pointed out
True
I think I would prefer to also use ?? in the type.
Okay, it's consistent, I'm down!
@Brendan Hansknecht that seems to be what you want as well, right?
If so, I'll give this a bit of a waiting period, and then start making some GitHub issues for implementation
So in the effectful future Result.mapErr is effectful? and hence we can do the logging type things in there
hm I think ? is better in the type
Luke Boswell said:
So in the effectful future
Result.mapErris effectful? and hence we can do the logging type things in there
no, but ? doesn't have to desugar to Result.mapErr - it can desugar to a when, which can do effects (or not) as desired
try, ?, and ?? will all have to be secretly effect polymorphic
In the way that if-then and when are the same
Richard Feldman said:
hm I think
?is better in the type
Even if has a different meaning at the usage point?
For example, this usage is a bug:
fn : { a ? Str } -> Str
fn = \a ? "Default" ->
a
well effect polymorphism only matters for functions
syntax sugar that desugars into something that isn't a function doesn't need to be polymorphic
it just works :big_smile:
Currently, ? desugars to a function
oh sure, but we could change that
But try would have to be handled "specially", yes
and same with the new ? and ??
That's why I say "secretly effect polymorphic"
Proposal 1: desugar ? to Result.mapErr :check:
I support this proposal as written. I like that this reduces the friction to tag errors and will promote layering additional context.
Proposal 2: desugar ?? to Result.withDefault :check:
I support this proposal as written. It's much easier to type and looks simple to understand.
Proposal 3: default record Type and Valued definition
I think we should use ?? for both the Type and Value definition for default values in records. Alternatively, I also think it would be fine to keep ? for the type and use ?? for the value.
However we handle it, they need to work for pure and effectful functions without forcing pure usages to "color" as effectful
/poll Which syntax would you rather have for "defaultable" fields in types?
? (current, simpler)
?? (new, matches default extraction syntax)
This is the last point of disagreement it seems
so when I see this:
foo ?? 0
I think "either foo or else `0``
so when I see this...
{ foo ?? Str }
I think "either foo or else Str"
so it's not quite the same as ?? - it's more like "this is maybe missing, but if it's present, it has to be Str"
which is fine, and I think that design is reasonable
but the concern that the (obviously more concise) status quo of:
{ foo ? Str }
...doesn't line up with the ?? usage doesn't resonate strongly with me because they don't really mean the same thing
in other words, regardless of whether we use ? or ?? for the type, either way it has its own meaning that's related to the expression-level ?? but not the same
Yes, it's slightly off. I think we're leaning towards ?? because it "has to do" with the other ??, and ? means "map error" elsewhere. I agree with you that the second point is a weak one
still, I do think there's a reasonable case to be made for the symmetry here
yeah like:
fn : { foo ?? Str } -> Str
fn = \{ foo ?? "" } ->
as opposed to:
fn : { foo ? Str } -> Str
fn = \{ foo ?? "" } ->
so I get that argument
I just imagine a lot of beginner bugs and confusion if they are different
hm, that's possible :thinking:
It'll be a syntax error either way
Not necessarily a problem, and we can make a specific error, but easy mistake to be a continual minor annoyance
I think we just need a "do you mean ??" message
true, but there's still the question of looking at it and understanding what it does
I actually removed my vote because I'm kinda 50-50, I'll leave the vote for anyone that has a real leaning
and I guess on that note, there's also the point that we've discovered that calling them "default record fields" eliminates some confusion compared to calling them "optional fields"
and ?? is more associated with defaults
compared to ? which is more associated with optionality (in more languages)
so that's another point in favor of ?? when it comes to learning
ok those points put it over the edge for me, I'm game to try it! :thumbs_up:
Cool!
I'll make the GitHub issues tonight
sweet, thanks!
Improve Error Handling Ergonomics: https://github.com/roc-lang/roc/issues/7086
Here's the parent issue, I've pinned it in the issues tab
Feel free to review and let me know if something should be changed
And reach out in this thread, on GitHub, or in my DMs if you feel like picking up one of these changes
try is probably the hardest, ?? for default fields should be the easiest
What about ?: for the type?
Button a : {
children : List (Element a),
onHover ?: HoverEvent -> Update a,
onDrop! : DropEvent => Update a,
onClick! ?: ClickEvent => Update a,
style ?: List Style,
}
: being in types.It does mean that ?: in the type and ?? in the variable binding are different, but they still both have a ? and appear related.
Sky Rose said:
What about
?:for the type?Button a : { children : List (Element a), onHover ?: HoverEvent -> Update a, onDrop! : DropEvent => Update a, onClick! ?: ClickEvent => Update a, style ?: List Style, }
- The space means it's not an interrobang and avoids making it seem like the name has a ?
- It keeps the meaning of
:being in types.It does mean that
?:in the type and??in the variable binding are different, but they still both have a ? and appear related.
I'm about 50-50 on this compared to ??, I think they have roughly equal merit. Since we already committed to ??, I'll default to that unless someone else pushes against ?? In favor of ?:
I'm not sure if i'm a little bit late to the party, but formatting for this feels wrong to me
observedFileData =
try Tracing.span "get-file-data" \{} =>
File.read! "some-file.txt" ? FileReadErr
# is `? FileReadErr`part of the lambda or the second part of `try`? at least I read it like this
observedFileData =
try Tracing.span "get-file-data" \{} =>
(File.read! "some-file.txt" ? FileReadErr )
# different formatting
observedFileData =
try Tracing.span "get-file-data" \{} =>
File.read! "some-file.txt"
? FileReadErr
You're right, the second parenthetical is how we plan to parse this
In that a ? errorMapper suffix is meant to bind tighter than other control flow
Oh, I thought try..? was a one construct. I believe now it makes more sense
Great!
Do I recall correctly that Roc is trying to avoid Option/Maybe in favor of Result/tagged unions? If so this is irrelevant, but if Option/Maybe is a thing in Roc, can I use ? to transform, and try to early-return, a None?
Yes, option is explicitly not in the std lib
You can still use ? to "eat" an error. For example, if we are calling File.read! Str => Result Str [NotFound], you could call:
File.read! "file.txt" ? \NotFound -> FileReadErr
This doesn't wrap the error, it consumes it and gives a better alternative
Thank you! I've been reading the issues and found myself asking this:
IIUC the two try below will propagate to the result of the pipeline such that fileContents will be a Result ... rather than a Str. Can I use try to keep fileContents : Str rather than have it be a result?
# Should not compile as fileContents will be a Result, not Str
fileContents : Str =
try readFromFile! filePath ? FileReadError filePath
|> modifyContentsSomehow
|> try takeASwigOfTheWhiskey!
|> ensureEndsWithNewline
@Niclas Ahden we're discussing whether it should be an early return or not at the moment, and leaning towards yes. So with good likelihood, fileContents will be a Str
I was thinking if there’s some middle-ground. Brendan mentioned that it could be block-level and Richard mentioned that’d mean we’d have to tryon control-flow like if else. Perhaps there’s a rule like “until the first closure that returns a result” or something. Then you could use it in an if and it’d still go to the function the if is in. If you make the if an assignment, with an explicit Result type, it’d stop there.
perhaps this is ludicrous, but it’d be nice to have a way to control it other than full early-return to the enclosing function.
I’ve started using try blocks in Rust and having that control is very ergonomic
I think there's a good reason to believe it'll be different in Roc vs Rust because in Roc it's so much easier to extract something into a function
in Roc you can always get the behavior of Rust's try { ... } blocks by doing (\{} -> ... ) {}
whereas in Rust if you try to do that with a closure, you're more likely than not to end up with a borrow checker and/or async error :sweat_smile:
That's probably very true, and I trust you core people's judgement. You've all made great decisions and it's a pleasure reading the discussions et al. I just got back to writing some Roc and it's divine even in this early stage. I can't wait until I can rewrite more services in it. Thanks for all the effort everyone!
thanks, I'm so glad you've been enjoying it while building useful things with it! :smiley:
I just now read this thread - in the middle I felt that the ? and ?? should be swapped (like Sam Mohr suggested) because "throwing" seems more intense than "proceeding", but after realizing that ? would become independent from try, I like this "v2" proposal as is. It feels like question marks would become synonymous with error handling transformations, where one mark means an error gets enhanced/recontextualized and two marks means an error gets ignored/dismissed/overridden/swallowed, which is more intense & significant from a safety & robustness perspective.
I think the main motivation for keeping ? from Rust and ?? from JS is because, well, that's what those operators look like in other languages
Last updated: Jun 16 2026 at 16:19 UTC