Stream: ideas

Topic: `try-else` error context adding syntax


view this post on Zulip Sam Mohr (Sep 13 2024 at 14:59):

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

view this post on Zulip Johan Lövgren (Sep 13 2024 at 16:06):

I like this. I Always felt a bit awkward with mapErr, even though it is perfectly sensible

view this post on Zulip Brendan Hansknecht (Sep 13 2024 at 16:08):

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

view this post on Zulip Johan Lövgren (Sep 13 2024 at 16:08):

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 -> …

view this post on Zulip Johan Lövgren (Sep 13 2024 at 16:10):

Though I guess handle, as you suggest I think, is also quite nice

view this post on Zulip Sam Mohr (Sep 13 2024 at 16:10):

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

view this post on Zulip Brendan Hansknecht (Sep 13 2024 at 16:11):

Almost feels like we should just commit to try catch, but:

  1. Catch doesn't sound like it is propagating the error again without another try. Kinda like how in other languages you catch and then throw again
  2. It won't catch crash....which sounds odd

view this post on Zulip Brendan Hansknecht (Sep 13 2024 at 16:13):

Of the options, I like else the best so far.

view this post on Zulip Johan Lövgren (Sep 13 2024 at 16:22):

Good points.

view this post on Zulip Johan Lövgren (Sep 13 2024 at 16:23):

I like else as well. It does read well.

view this post on Zulip Sky Rose (Sep 14 2024 at 12:54):

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?)

view this post on Zulip Sky Rose (Sep 14 2024 at 12:56):

Can you pipe a result into try?

jsonData =
  File.read! "some-file.txt"
  |> try else FileReadErr
  |> Json.decode
  |> try else JsonDecodeErr

view this post on Zulip Sam Mohr (Sep 14 2024 at 15:49):

Sky Rose said:

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?)

Else is not required, and the parser will consider else part of the try expression, it's pretty well structured.

view this post on Zulip Sam Mohr (Sep 14 2024 at 16:03):

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

view this post on Zulip Sam Mohr (Sep 14 2024 at 16:05):

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

view this post on Zulip Brendan Hansknecht (Sep 14 2024 at 16:11):

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

view this post on Zulip Sam Mohr (Sep 14 2024 at 16:47):

Yeah, probably shouldn't support piping to it, then

view this post on Zulip Sam Mohr (Sep 14 2024 at 17:11):

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

view this post on Zulip Sam Mohr (Sep 14 2024 at 17:30):

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

view this post on Zulip Sky Rose (Sep 14 2024 at 17:43):

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.

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:01):

a downside of else I realized is that it's coupled to try, whereas alternatives (such as an infix operator like ?>) can work standalone

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:02):

e.g. in the recent discussions about tracing spans, this can work:

fileContents =
    try Trace.span context \{} ->
        File.read! "some-file.txt" ?> ReadFailed

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:02):

...but else in the same place can't work because there's no try in that lambda

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:03):

(I'm not saying ?> is the way to go, just that an ordinary infix operator doesn't have this downside)

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:03):

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"

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:04):

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

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:05):

it also occurred to me that we could just use ? for this:

fileContents =
    try Trace.span context \{} ->
        File.read! "some-file.txt" ? ReadFailed

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:05):

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

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:06):

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

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:07):

I'd prefer ? because it doesn't have > after it, meaning it's not a piping operator

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:07):

And that means we'd use it inline instead of on the next line

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:08):

(for the most part)

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:13):

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

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:17):

Thinking through everything in the proposal including desugaring and syntax, I think I'd want everything to work the same

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:38):

yeah the binding rules seem to fit together nicely!

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:38):

there's a related question of whether we want an operator for |> Result.withDefault

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:39):

like some languages use ?: or ?? for that

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:39):

The TypeScript one is ??, Python/Ruby is or. I'd vote ?? for consistency if we go for ?

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:40):

Not sure if we need one, though

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:40):

Swift uses ??

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:41):

(that's where I heard about it first)

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:41):

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

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:41):

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 ?:

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:42):

See other threads in this topic if you want to show that people like fun syntaxes

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:42):

yeah I agree that it's not as important, but I've missed it compared to languages that have it

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:43):

The thing I want is a heuristic we can use to say "this is too many operators!"

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:43):

I don't have that, so I feel unsure

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:43):

But it's very useful, yes

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:44):

:thinking: I wonder if we should actually make that consistent with how default record fields work, e.g.

fn = \{ foo ?? 0, bar ?? 1 } ->

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:44):

it's almost literally "with default"

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:44):

hm, although actually that could be confusing

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:45):

in that it might suggest foo is a Result and you're defaulting away the Result in the pattern or something :thinking:

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:46):

No, I like that!

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:46):

It feels like slightly confusing is better than confusing

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:46):

? now means mapErr

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:47):

right

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:47):

So ?? is closer to "default value"

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:47):

should the type change too for consistency? :thinking:

{ foo ?? Str, bar ?? Str }

view this post on Zulip Richard Feldman (Sep 16 2024 at 02:48):

I guess that kinda looks like "foo's type defaults to Str" :sweat_smile:

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:49):

I'd say so

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:49):

For the same reason as above

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:49):

Unless we change for a different operator for mapErr

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:53):

Actually, we could just swap these two

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:53):

? now means default value, ?? means mapErr

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:55):

That preserves the current meaning of ?, meaning fewer changes

view this post on Zulip Sam Mohr (Sep 16 2024 at 02:55):

Though I think I'd prefer ?? or ?: for consistency with other languages

view this post on Zulip Luke Boswell (Sep 16 2024 at 02:58):

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.

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:00):

Sure, I can make a v2 with the new operators and more examples

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:00):

despite what I said earlier against ?: - there is actually something interesting about how : appears in records and types:

{ foo : Str, bar ?: Str }
fn = \{ bar ?: "" } ->

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:07):

It kind of implies "assign if necessary"

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:07):

Which is what we want

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:08):

that said, I still prefer ?? :laughing:

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:12):

I'll go with ?? for now, I agree

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:13):

Actually, issue: try Utc.now! ? FailedToGetTime

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:14):

We're pretty close to !?, that's what else avoids

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:14):

I don't have a problem with it if there's a space

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:14):

Ok, sure

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:14):

especially with syntax highlighting:

try Utc.now! ? FailedToGetTime

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:16):

there's an argument for ? should be withDefault and something else should be mapErr

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:16):

namely that withDefault should be consistent with how it works in records

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:16):

and that ? looks better than ?? in record types

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:17):

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

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:19):

Yeah, and I don't know another language that uses ? for default values in records

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:19):

So I'll propose we change default records to use ??

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:21):

maybe it's better to have it be ? in the type and ?? in the destructure :thinking:

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:21):

like { foo ? Str, bar ? Str } and then fn = \{ foo ?? "", bar ?? "" } ->

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:25):

It should at least be in the destructure IMO

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:28):

I'll have to think on it, this system in its entirety isn't "falling into place" in my head, comfort-wise

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:29):

yeah

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:39):

certainly using ? as its current uses plus "with default" seems like a reasonable idea

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:39):

it just means finding another one for mapErr

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:39):

Yep

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:43):

We aren't using @ or $ really at the moment

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:44):

Those are your easy to type operators because they're number shifts

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:44):

@ is for opaque types

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:45):

So maybe $ works? It's already an arbitrary operator. I'd vote $ or a ? plus something else

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:50):

I could see doing ?? for mapErr and just explaining that it's kinda swapped

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:51):

but maybe that's easy for me to say when I'm not already used to it :laughing:

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:51):

Sure, though at that point, are we just swapping so that we can preserve default values in records?

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:51):

What's the reason to swap

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:52):

yeah exactly

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:52):

well not just for the sake of not changing it, but rather because I think { foo ? Str } reads better than { foo ?? Str }

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:53):

maybe I'd get used to it though

view this post on Zulip Richard Feldman (Sep 16 2024 at 03:53):

it doesn't come up that often, to be fair

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:57):

I always prefer consistency and orthogonality

view this post on Zulip Sam Mohr (Sep 16 2024 at 03:57):

So I think it's worth swallowing that bullet

view this post on Zulip Aurélien Geron (Sep 16 2024 at 07:00):

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.

view this post on Zulip Aurélien Geron (Sep 16 2024 at 07:08):

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.

view this post on Zulip Sam Mohr (Sep 16 2024 at 19:12):

@Sky Rose

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.

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?

view this post on Zulip Sam Mohr (Sep 16 2024 at 19:23):

@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 :

view this post on Zulip Sam Mohr (Sep 16 2024 at 19:24):

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 ??

view this post on Zulip Sam Mohr (Sep 16 2024 at 19:25):

As we're already planning on aliasing it to Result.withDefault in expressions

view this post on Zulip Aurélien Geron (Sep 16 2024 at 20:58):

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.

view this post on Zulip Sam Mohr (Sep 16 2024 at 21:13):

That's alignment! Tonight I'll make the v2 doc so we can all look over some code examples

view this post on Zulip Sam Mohr (Sep 17 2024 at 06:37):

Here's the second version of the doc! https://docs.google.com/document/d/1rb2gZNKKAwya2JQQyV9Tsjj_8K1p-P_-DgBXCI0WqOY/edit?usp=sharing

view this post on Zulip Sam Mohr (Sep 17 2024 at 06:38):

Since there are no new ideas in there, I'm avoiding making a new thread

view this post on Zulip Sam Mohr (Sep 17 2024 at 06:39):

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.

view this post on Zulip Sam Mohr (Sep 17 2024 at 06:42):

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,
}

view this post on Zulip Sam Mohr (Sep 17 2024 at 06:42):

So we should probably keep the current syntax for that, e.g. field ? Type

view this post on Zulip Sam Mohr (Sep 17 2024 at 06:43):

Otherwise, I kept the decisions from this thread in the doc:

view this post on Zulip Sam Mohr (Sep 17 2024 at 06:44):

I'll make some GitHub issues once we've agreed that v2 looks good

view this post on Zulip Anton (Sep 17 2024 at 08:46):

Thanks for setting up the doc @Sam Mohr, can you enable public access for the doc? I'm currently getting access denied

view this post on Zulip Sam Mohr (Sep 17 2024 at 10:10):

@Anton should be fixed

view this post on Zulip Luke Boswell (Sep 17 2024 at 10:11):

Working for me :thank_you:

view this post on Zulip Luke Boswell (Sep 17 2024 at 10:12):

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??

view this post on Zulip Luke Boswell (Sep 17 2024 at 10:22):

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"

view this post on Zulip Luke Boswell (Sep 17 2024 at 10:24):

Can we currently have default values for functions onHover ? HoverEvent -> Update a, I don't think I've seen this before.

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 13:45):

## 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.

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 13:46):

It will make it feel like a position! should be used as position, which is incorrect

view this post on Zulip Sam Mohr (Sep 17 2024 at 13:49):

Agreed

view this post on Zulip Sam Mohr (Sep 17 2024 at 13:49):

So I vote for keeping the syntax as it is right now

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 13:53):

Should it be changed to double ? In the type? ??

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 13:53):

Just to match the defaulting?

view this post on Zulip Notification Bot (Sep 17 2024 at 16:42):

41 messages were moved from this topic to #ideas > alternatives to try for errors by Brendan Hansknecht.

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 16:43):

Just to keep this discussion focused on try specifically.

view this post on Zulip Sam Mohr (Sep 17 2024 at 16:43):

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

view this post on Zulip Sam Mohr (Sep 17 2024 at 16:43):

(this was moved with the blob)

view this post on Zulip Anton (Sep 17 2024 at 17:46):

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?

view this post on Zulip Sam Mohr (Sep 17 2024 at 17:51):

@Anton I only mean using ? to mean "type of defaultable field", all other proposals in v2 would be kept.

view this post on Zulip Sam Mohr (Sep 17 2024 at 17:53):

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 } -> ...

view this post on Zulip Sam Mohr (Sep 17 2024 at 18:49):

Luke Boswell said:

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"

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).

view this post on Zulip Sam Mohr (Sep 17 2024 at 18:49):

Also, you are the only one with edit access

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 18:54):

If we only allow ? to be used with try it would be impossible to use it with ?? cause try will propagate the error case.

view this post on Zulip Sam Mohr (Sep 17 2024 at 18:58):

Well, except for nested Results. Always a gross case to support, but it'll happen

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 19:06):

Sure, but normally a type error and when you see it, it means it has to be a nested result. Sounds fine

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:08):

I realize I missed something in your comment

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:08):

We changed to ? over else so we could use it without try

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:08):

Say

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:09):

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

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:09):

That needs to be preserved IMO

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:09):

If we dislike that, I think we should go back to else

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 19:39):

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

view this post on Zulip Richard Feldman (Sep 17 2024 at 19:41):

but sometimes I might want to use ? without using try in a position where try would short-circuit

view this post on Zulip Richard Feldman (Sep 17 2024 at 19:41):

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

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:43):

We always want to run effects in the current system, which is why ! is now glued onto names

view this post on Zulip Sam Mohr (Sep 17 2024 at 19:44):

Not true of results IMO, for the reasons Richard pointed out

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 19:44):

True

view this post on Zulip Luke Boswell (Sep 17 2024 at 22:46):

I think I would prefer to also use ?? in the type.

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:47):

Okay, it's consistent, I'm down!

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:47):

@Brendan Hansknecht that seems to be what you want as well, right?

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:48):

If so, I'll give this a bit of a waiting period, and then start making some GitHub issues for implementation

view this post on Zulip Luke Boswell (Sep 17 2024 at 22:50):

So in the effectful future Result.mapErr is effectful? and hence we can do the logging type things in there

view this post on Zulip Richard Feldman (Sep 17 2024 at 22:51):

hm I think ? is better in the type

view this post on Zulip Richard Feldman (Sep 17 2024 at 22:52):

Luke Boswell said:

So in the effectful future Result.mapErr is 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

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:53):

try, ?, and ?? will all have to be secretly effect polymorphic

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:53):

In the way that if-then and when are the same

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 22:55):

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

view this post on Zulip Richard Feldman (Sep 17 2024 at 22:55):

well effect polymorphism only matters for functions

view this post on Zulip Richard Feldman (Sep 17 2024 at 22:55):

syntax sugar that desugars into something that isn't a function doesn't need to be polymorphic

view this post on Zulip Richard Feldman (Sep 17 2024 at 22:55):

it just works :big_smile:

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:56):

Currently, ? desugars to a function

view this post on Zulip Richard Feldman (Sep 17 2024 at 22:56):

oh sure, but we could change that

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:56):

But try would have to be handled "specially", yes

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:56):

and same with the new ? and ??

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:56):

That's why I say "secretly effect polymorphic"

view this post on Zulip Luke Boswell (Sep 17 2024 at 22:57):

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:57):

However we handle it, they need to work for pure and effectful functions without forcing pure usages to "color" as effectful

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:58):

/poll Which syntax would you rather have for "defaultable" fields in types?
? (current, simpler)
?? (new, matches default extraction syntax)

view this post on Zulip Sam Mohr (Sep 17 2024 at 22:59):

This is the last point of disagreement it seems

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:34):

so when I see this:

foo ?? 0

I think "either foo or else `0``

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:34):

so when I see this...

{ foo ?? Str }

I think "either foo or else Str"

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:35):

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"

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:35):

which is fine, and I think that design is reasonable

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:36):

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

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:37):

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

view this post on Zulip Sam Mohr (Sep 17 2024 at 23:37):

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

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:37):

still, I do think there's a reasonable case to be made for the symmetry here

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:38):

yeah like:

fn : { foo ?? Str } -> Str
fn = \{ foo ?? "" } ->

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:38):

as opposed to:

fn : { foo ? Str } -> Str
fn = \{ foo ?? "" } ->

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:38):

so I get that argument

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 23:38):

I just imagine a lot of beginner bugs and confusion if they are different

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:38):

hm, that's possible :thinking:

view this post on Zulip Sam Mohr (Sep 17 2024 at 23:38):

It'll be a syntax error either way

view this post on Zulip Brendan Hansknecht (Sep 17 2024 at 23:39):

Not necessarily a problem, and we can make a specific error, but easy mistake to be a continual minor annoyance

view this post on Zulip Sam Mohr (Sep 17 2024 at 23:39):

I think we just need a "do you mean ??" message

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:39):

true, but there's still the question of looking at it and understanding what it does

view this post on Zulip Sam Mohr (Sep 17 2024 at 23:39):

I actually removed my vote because I'm kinda 50-50, I'll leave the vote for anyone that has a real leaning

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:39):

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"

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:39):

and ?? is more associated with defaults

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:40):

compared to ? which is more associated with optionality (in more languages)

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:40):

so that's another point in favor of ?? when it comes to learning

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:40):

ok those points put it over the edge for me, I'm game to try it! :thumbs_up:

view this post on Zulip Sam Mohr (Sep 17 2024 at 23:40):

Cool!

view this post on Zulip Sam Mohr (Sep 17 2024 at 23:40):

I'll make the GitHub issues tonight

view this post on Zulip Richard Feldman (Sep 17 2024 at 23:48):

sweet, thanks!

view this post on Zulip Sam Mohr (Sep 18 2024 at 04:26):

Improve Error Handling Ergonomics: https://github.com/roc-lang/roc/issues/7086

view this post on Zulip Sam Mohr (Sep 18 2024 at 04:27):

Here's the parent issue, I've pinned it in the issues tab

view this post on Zulip Sam Mohr (Sep 18 2024 at 04:27):

Feel free to review and let me know if something should be changed

view this post on Zulip Sam Mohr (Sep 18 2024 at 04:29):

And reach out in this thread, on GitHub, or in my DMs if you feel like picking up one of these changes

view this post on Zulip Sam Mohr (Sep 18 2024 at 04:30):

try is probably the hardest, ?? for default fields should be the easiest

view this post on Zulip Sky Rose (Sep 18 2024 at 12:52):

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,
}

It does mean that ?: in the type and ?? in the variable binding are different, but they still both have a ? and appear related.

view this post on Zulip Sam Mohr (Sep 18 2024 at 16:14):

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,
}

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 ?:

view this post on Zulip Chris (Sep 19 2024 at 09:55):

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

view this post on Zulip Sam Mohr (Sep 19 2024 at 09:56):

You're right, the second parenthetical is how we plan to parse this

view this post on Zulip Sam Mohr (Sep 19 2024 at 09:57):

In that a ? errorMapper suffix is meant to bind tighter than other control flow

view this post on Zulip Chris (Sep 19 2024 at 09:59):

Oh, I thought try..? was a one construct. I believe now it makes more sense

view this post on Zulip Sam Mohr (Sep 19 2024 at 09:59):

Great!

view this post on Zulip Niclas Ahden (Sep 19 2024 at 10:50):

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?

view this post on Zulip Sam Mohr (Sep 19 2024 at 10:53):

Yes, option is explicitly not in the std lib

view this post on Zulip Sam Mohr (Sep 19 2024 at 10:55):

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

view this post on Zulip Sam Mohr (Sep 19 2024 at 10:56):

This doesn't wrap the error, it consumes it and gives a better alternative

view this post on Zulip Niclas Ahden (Sep 19 2024 at 12:21):

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

view this post on Zulip Sam Mohr (Sep 19 2024 at 16:04):

@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

view this post on Zulip Niclas Ahden (Sep 19 2024 at 16:12):

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.

view this post on Zulip Niclas Ahden (Sep 19 2024 at 16:13):

I’ve started using try blocks in Rust and having that control is very ergonomic

view this post on Zulip Richard Feldman (Sep 19 2024 at 16:29):

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

view this post on Zulip Richard Feldman (Sep 19 2024 at 16:29):

in Roc you can always get the behavior of Rust's try { ... } blocks by doing (\{} -> ... ) {}

view this post on Zulip Richard Feldman (Sep 19 2024 at 16:30):

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:

view this post on Zulip Niclas Ahden (Sep 19 2024 at 16:47):

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!

view this post on Zulip Richard Feldman (Sep 19 2024 at 16:51):

thanks, I'm so glad you've been enjoying it while building useful things with it! :smiley:

view this post on Zulip jan kili (Dec 28 2024 at 15:20):

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.

view this post on Zulip Sam Mohr (Dec 28 2024 at 15:32):

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