a nice use of the ??
operator with early return
:
method_and_path = request.method_and_path() ??
return Response.err(400).body("Bad HTTP method: ${method}")
this works because ??
doesn't desugar to with_default
but rather to this:
method_and_path =
when request.method_and_path() is
Ok(method_and_path) -> method_and_path
Err(_) -> return Response.err(400).body("…")
Is that a feature or a bug?
Like isn't that just the binop version ?
? Just reimplemented via ??
?
Oh, this is subtly different. Binop for ?
wraps and error and this must return an error.
yeah, binop ?
is if you want to do something with the Err
which this one doesn't
This is returning a success. It is just a success that returns a 400 http code
also, binop ?
takes a lambda, so early return
wouldn't work with it
right, this whole function has the type Request => Response
Yep
so it's nice to be able to turn a Result
into a Response
like this! :smiley:
especially right up front without indenting
(the classic "early return" benefit)
It's a useful variant of Result.onErr
that is already unwrapped
well Result.on_err
couldn't work here
because it takes a lambda
and return
inside the lambda returns from the lambda, not from the outer function (which is what we want here)
Yeah, but it might solve _some_ of the same usecases, but with less noise
oh sure, it's great to have both
And plus the early return which is nice
like this wouldn't work in the middle of a pipeline of calls
but for early return, it's definitely nice!
Yeah, if you made errors explicit, it would be something like:
method_and_path = request.method_and_path() ?
|_| { err_code: 400, body: "..." }
yeah if the function returned Result Response ...
then that would work
but in this case it's desirable to have it return Response
instead of Result
Yeah
Cool here's another good usecase for ??
@Anton :smile:
This looks a bit like Zig's catch
keyword, which I'm quite fond of. catch
allows capturing the error value though.
If I came across this pattern while reviewing code I think I would add a note to it, because I think it's almost always a mistake to drop an error value. When the error value is not incorporated into the return value somehow I'd expect it to get logged, so there's some trail to follow when debugging. For instance, in the 400 response example above, to help figure out why a server is returning an unexpected 400 error.
in this case that method only returns one error
so there isn't any relevant info to drop here
Fair point, but:
It comes down to the sugar containing an Err(_)
branch and so using it means giving up on some exhaustiveness checking with all the associated risks and benefits.
For calling effectful functions, I believe we have the rule that if you're not interested in the return value, you have to do:
_ = runEffect!()
And I believe the plan is that the _ =
can be ommitted if the return value is {}
.
Is this the same situation where we want to help against accidentally dropping information? Or do we feel differently because it's an error value?
What if the 'signature' of the ??
operator (and possibly Result.withDefault
as well) becomes:
(??) : Result val err, (err => val) => val
method_and_path = request.method_and_path() ?? |_|
return Response.err(400).body("Bad HTTP method: ${method}")
And make it so the |_|
can be ommitted if the error type is {}
?
hm, allowing the lambda to be omitted based on type information would mean this is no longer syntax sugar, and feels too complicated to me :sweat_smile:
and if it couldn't be omitted, then the early return wouldn't work here
Richard Feldman said:
and if it couldn't be omitted, then the early return wouldn't work here
Is this because the return
from return from the lambda instead of the containing function?
Maybe ?? |x| ...
would constitute one sugar, so the whole thing is a 'fake lambda' with slightly different rules, but that sounds really confusing :sweat_smile:. Maybe there's a different syntax that does the same thing but without implying lambda-ness.
Personally, without the error capture I'd consider ??
a bit of a footgun, and would rather not have it, but I appreciate others might feel different :).
Yeah, remember in Zig, ||
is a capture group, not a lambda. It's still a part of the containing function
Jasper Woudenberg said:
It comes down to the sugar containing an
Err(_)
branch and so using it means giving up on some exhaustiveness checking with all the associated risks and benefits.
This confuses me a bit. The whole point of something like Result.withDefault
is to remove the error case. If you are moving to the ok case only, I feel like it would be an antipattern to still depend on the error.
Dict.get dict key ?? default_value
Anthony Bullard said:
Yeah, remember in Zig,
||
is a capture group, not a lambda. It's still a part of the containing function
I feel like that would lead to a lot of confusion and inconsistency in roc. In zig, it is a capture group cause they have no lambdas. They are a low level language with only function pointers.
That was exactly my point, different languages with different idioms and constructs
Ah, I thought you were suggesting we could do the same thing as zig. Definitely a misunderstanding of intent.
Zig also has syntax in ifs for unwrapping optionals
We should do us
If you want to do something with the error, we have onErr, mapErr, and mapBoth
Jasper Woudenberg said:
When the error value is not incorporated into the return value somehow I'd expect it to get logged, so there's some trail to follow when debugging.
Ah, I see your reasoning. I guess I rarely work on code where this is the case. Generally the with default is a semi-expected case and the error is not something to care about. Something akin to Dict.get
with a default value being the most common use case.
Even in most zig code where I use catch it is generally catch unreachable
or similar.
I can see how for high level logic like deciding an http 400 you might want logging or ab explicit not of no extra info to log. For me that is the exceptional case not the common case, so I didn't think of it.
I didn't realize before that this "problem" (depending on how you feel about it) is inherent to Result.withDefault
as well.
Brendan Hansknecht said:
Even in most zig code where I use catch it is generally
catch unreachable
or similar.
I do that too sometimes, but I think that's a different case. In case of a catch unreachable
we don't take the error branch. I think the Zig equivalent of ??
would be catch return
.
Yeah, as I said as my first comment:
Is that a feature or a bug?
??
just forcing a default value makes sense to me. Using it with return
feels much more accidental. Maybe it is good. Maybe it leads to ignoring error classes.
Brendan Hansknecht said:
Something akin to
Dict.get
with a default value being the most common use case.
Yeah, that use doesn't seem particularly bad to me either, the Dict.get
error is essentially equivalent to {}
, so we're not losing any information.
Last updated: Jul 06 2025 at 12:14 UTC