lang (syntax | error-handling | keyword)
try_expr
RFC 243 left the choice of keyword for catch { .. }
expressions unresolved.
This RFC settles the choice of keyword. Namely, it:
try
as a keyword in edition 2018.do catch { .. }
with try { .. }
catch
as a keyword.This RFC does not motivate catch { .. }
or try { .. }
expressions.
To read the motivation for that, please consult the original catch
RFC.
Whatever keyword is chosen, it can't be contextual.
As with catch { .. }
, the syntactic form <word> { .. }
where <word>
is replaced with any possible keyword would conflict with a struct named
<word>
as seen in this perfectly legal snippet in Rust 2015,
where <word>
has been substituted for try
:
struct try;
fn main() {
try {
};
}
The snippet above emits the following warning:
warning: type `try` should have a camel case name such as `Try`
which is also the case for catch
.
This warning decreases the risk that someone has defined a type named try
anywhere in the ecosystem which happens to be beneficial to us.
try
specificallyThis is discussed in the rationale for try
.
The keyword try
will be reserved.
This will allow you to write expressions such as:
try {
let x = foo?;
let y = bar?;
// Note: OK-wrapping is assumed here, but it is not the goal of this RFC
// to decide either in favor or against OK-wrapping.
x + y
}
The word try
is reserved as a keyword in the list of keywords
in Rust edition 2018 and later editions.
The keyword try
is used in "try expressions" of the form try { .. }
.
There are two main drawbacks to the try
keyword.
I think that there is a belief – one that I have shared from time to time – that it is not helpful to use familiar keywords unless the semantics are a perfect match, the concern being that they will setup an intuition that will lead people astray. I think that is a danger, but it works both ways: those intuitions also help people to understand, particularly in the early days. So it’s a question of “how far along will you get before the differences start to matter” and “how damaging is it if you misunderstand for a while”.
[..]
Rust has a lot of concepts to learn. If we are going to succeed, it’s essential that people can learn them a bit at a time, and that we not throw everything at you at once. I think we should always be on the lookout for places where we can build on intuitions from other languages; it doesn’t have to be a 100% match to be useful.
For some people, the association to try { .. } catch { .. }
in languages such
as Java, and others in the prior-art section, is unhelpful wrt. teachability
because they see the explicit, reified, and manually propagated exceptions in
Rust as something very different than the much more implicit exception handling
stories in Java et al.
However, we make the case that other languages which do have these explicit and
reified exceptions as in Rust also use an exception vocabulary.
Notably, Haskell calls the monad-transformer for adding exceptions ExceptT
.
We also argue that even tho we are propagating exceptions manually, we are following tradition in that other languages have very different formulations of the exception idea.
The benefit of familiarity, even if not a perfect match, as Niko puts it, helps in learning, particularly because Rust is not a language in lack of concepts to learn.
try!
macroOne possible result of introducing try
as a keyword be that the old try!
macro would break. This could potentially be avoided but with great technical
challenges.
With the prospect of breaking try!
, a few notes are in order:
?
was stabilized in 1.13, November 2016, which is roughly 1.4 years since
the date this RFC was started.try!
has been "deprecated" since then since:
The
?
operator was added to replacetry!
and should be used instead.
try!(expr)
can in virtually all instances be automatically rustfix
ed
automatically to expr?
.try!
.try!
.So overall I think it’s feasible to reduce the
try!
macro to a historical curiosity to the point it won’t be actively confusing to newbies coming to Rust.
- kornel
However,
try!
.try!
is essentially the inverse of try { .. }
.Purging from the “collective memories of Rustaceans and Rust materials” is not something that easy.
In the RFC author's opinion however, the sum total benefits of try { .. }
seem to outweigh the drawbacks of the difficulty with purging try!
from
our collective memory.
?
The ?
postfix operator is sometimes referred to as the "try operator",
and can be seen as having the inverse semantics as try { .. }
.
To many, this is a drawback. To others, this makes the ?
and try { .. }
expression forms more closely related and therefore makes them more findable
in relation to each other.
There is currently some ongoing debate about renaming the ?
operator to
something other than the "try operator". This could help in mitigating the
effects of picking try
as the keyword.
Among the considerations when picking a keyword are, ordered by importance:
Fidelity to the construct's actual behavior.
Precedent from existing languages
See the prior art the rationale for try for more discussion on precedent.
Brevity.
Consistency with related standard library function conventions.
Consistency with the naming of the trait used for ?
(the Try
trait).
Since the Try
trait is unstable and the naming of the ?
operator in
communication is still unsettled, this is not regarded as very important.
Degree / Risk of breakage.
Consistency with old learning material.
That is, (in)consistency with ?
and the try!()
macro.
If the first clause is called try
,
then try { }
and try!()
would have essentially inverse meanings.
try
?
: Consistenttry!
will break, otherwise: Low)
std::try!
, but it is technically possible to not break this macro. (unstable: std::intrinsics::try
so irrelevant)repogroup:crates case:yes max:400
\b((let|const|type|)\s+try\s+=|(fn|impl|mod|struct|enum|union|trait)\s+try)\b
try!
)This is our choice of keyword, because it:
try
means in those languages.
Thus, we can leverage people's intuitions and not spend too much of our
complexity budget.Try
and try_
prefixed methods.?
). This high fidelity is from the perspective of
a programmers intent, i.e: "I want to try a bunch of stuff in this block".catch { .. }
handlers if we wish.catch
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+catch\s+=|(fn|impl|mod|struct|enum|union|trait)\s+catch)\b
We believe catch
to be a poor choice of keyword, because it:
catch(pat) { recover_expr }
.catch
with handlers will require a different word such as
handler
to get catch { .. } handler(e) { .. }
semantics if we want.
This inversion compared to a lot of other languages will only harm
teachability of the language and steal a lot of our strangeness budget.try
.catch_unwind
, but that has to do with panics,
not Try
style exceptions.However, catch
has high fidelity wrt. the operational semantics of "catching"
any exceptions in the try { .. }
block.
do catch { .. }
do
: Haskell, Idriscatch
: Erlang and Tcl, see prior-art?
: Inconsistent$ident $ident
is not a legal identifier.An alternative would be to simply use the do catch { ... }
syntax we have
in the nightly compiler. However, this syntax was not in the accepted catch
RFC and was only a temporarly fix around catch { .. }
not working.
do try { .. }
do
: Haskell, Idristry
: A lot, see prior-art?
: Moderately consistent$ident $ident
is not a legal identifier.We could in fact decide to keep the do
-prefix but change the suffix to try
.
The benefit here would be two-fold:
No keyword try
would need to be introduced as do
already is a keyword.
Therefore, the try!
macro would not break.
An association with monads due to do
. This can be considered a benfit since
try
can be seen as sugar for the family of error monads
(modulo kinks wrt. imperative flow), and thus,
the do
prefix leads to a path of generality if more monads are introduced.
The drawbacks would be:
The wider association with monads can be seen as a drawback for those not familiar with monads.
do try { .. }
over try { .. }
adds a small degree of ergonomics overhead
but not much (3 characters including the space). However, the frequency with
which the try { .. }
construct might be used can make the small overhead
accumulate to a significant overhead when a large codebase is considered.
Other than this, the argument for do try
over do catch
boils down to an
argument of try
over catch
.
do { .. }
?
: InconsistentThe keyword do
was probably originally reserved for two use cases:
do while { .. }
Monadic do
-notation a la Haskell:
stuff = do
x <- actionX
y <- actionY x
z <- actionZ
sideEffect
finalAction x y z
The which would be translated into the following pseudo-Rust:
let stuff = do {
x <- actionX;
y <- actionY(x);
z <- actionZ;
sideEffect;
finalAction(x, y, z);
};
Or particularly for the try { .. }
case:
let stuff = try {
let x = actionX?;
let y = actionY(x)?;
let z = actionZ?;
sideEffect?;
finalAction(x, y, z)
};
The Haskell version is syntactic sugar for:
stuff = actionX >>=
\x -> actionY x >>=
\y -> actionZ >>=
\z -> sideEffect >>
finalAction x y z
or in Rust:
let stuff =
actionX.flat_map(|x| // or .and_then(..)
actionY(x).flat_map(|y|
actionZ.flat_map(|z|
sideEffect.flat_map(|_|
finalAction(x, y, z)
)
)
)
);
In the Haskell version, >>=
is defined in the Monad
typeclass (trait):
{-# LANGUAGE KindSignatures #-}
class Applicative m => Monad (m :: * -> *) where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
(>>) = \ma mb -> ma >>= \_ -> mb
And some instances (impls) of Monad
are:
-- | Same as Option<T>
data Maybe a = Nothing | Just a
instance Monad Maybe where
return = Just
(Just a) >>= f = f a
_ >>= _ = Nothing
-- | `struct Norm<T> { value: T, normalized: bool }`
data Norm a = Norm a Bool
instance Monad Norm where
return a = Norm a False
(Norm a u) >>= f = let Norm b w = f a in Norm b (u || w)
Considering the latter case of do-notation,
we saw how try { .. }
and do { .. }
relate.
In fact, try { .. }
is special to the Try
(MonadError
) monads.
There are also more forms of monads which you might want to use do { .. }
for.
Among these are: Futures, Iterators
Due to having more monads than Try
-based ones,
using the do { .. }
syntax directly as a replacement for try { .. }
becomes
problematic as it:
do
is generic and unclear wrt. semantics.trap
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+trap\s+=|(fn|impl|mod|struct|enum|union|trait)\s+trap)\b
Arguably, this candidate keyword is a somewhat a good choice.
To trap
an error is sufficently clear on the "exception boundary" semantics
we wish to communicate.
However, trap
is used as an error handler in at least one langauge.
It also does not have the familiarity that try
does have and is entirely
inconsistent wrt. naming in the standard library.
wrap
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+wrap\s+=|(fn|impl|mod|struct|enum|union|trait)\s+wrap)\b
With wrap { .. }
we can say that it "wraps" the result of the block as a
Result
/ Option
, etc. and it is logically related to .unwrap()
,
which is however a partial function, wherefore the connotation might be bad.
Also, wrap
could be considered too generic as with do
in that it could
fit for any monad.
result
?
: Inconsistent{std, core}::result
modules.repogroup:crates case:yes max:400
\b((let|const|type|)\s+result\s+=|(fn|impl|mod|struct|enum|union|trait)\s+result)\b
The fidelity of result
is somewhat good due to the association with the
Result
type as well as Try
being a final encoding of Result
.
However, when you consider Option
, the association is less direct,
and thus it does not fit Option
and other types well.
The breakage of the result
module is however quite problematic,
making this particular choice of keyword more or less a non-starter.
There are a host of other keywords which have been suggested.
fallible
On an internals thread, fallible
was suggested. However, this keyword lacks the verb-form that
is the convention in Rust. Breaking with this convention should only be done
if there are significant reasons to do so, which do not seem to exist in this
case. It is also considerably longer than try
(+5 character) which matters
for constructions which are oft used.
?
: Inconsistentcatch
:Some synonyms of catch
have been suggested:
accept
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+accept\s+=|(fn|impl|mod|struct|enum|union|trait)\s+accept)\b
capture
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+capture\s+=|(fn|impl|mod|struct|enum|union|trait)\s+capture)\b
collect
?
: InconsistentIterator::collect
)repogroup:crates case:yes max:400
\b((let|const|type|)\s+collect\s+=|(fn|impl|mod|struct|enum|union|trait)\s+collect)\b
recover
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+recover\s+=|(fn|impl|mod|struct|enum|union|trait)\s+recover)\b
resolve
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+resolve\s+=|(fn|impl|mod|struct|enum|union|trait)\s+resolve)\b
take
?
: Inconsistent{Cell, HashSet, Read, Iterator, Option}::take
.repogroup:crates case:yes max:400
\b((let|const|type|)\s+take\s+=|(fn|impl|mod|struct|enum|union|trait)\s+take)\b
Of these, only recover
and capture
seem reasonable semantically.
But recover
is even more problematic than catch
because it enhances
the feeling of exception-handling instead of exception-boundaries.
However, capture
is reasonable as a substitute for try
,
but it seems obscure and lacks familiarity, which is counted as a strong downside.
coalesce
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+coalesce\s+=|(fn|impl|mod|struct|enum|union|trait)\s+coalesce)\b
fuse
?
: InconsistentIterator::fuse
.repogroup:crates case:yes max:400
\b((let|const|type|)\s+fuse\s+=|(fn|impl|mod|struct|enum|union|trait)\s+fuse)\b
unite
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+unite\s+=|(fn|impl|mod|struct|enum|union|trait)\s+unite)\b
cohere
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+cohere\s+=|(fn|impl|mod|struct|enum|union|trait)\s+cohere)\b
consolidate
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+consolidate\s+=|(fn|impl|mod|struct|enum|union|trait)\s+consolidate)\b
unify
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+take\s+=|(fn|impl|mod|struct|enum|union|trait)\s+take)\b
combine
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+combine\s+=|(fn|impl|mod|struct|enum|union|trait)\s+combine)\b
resultof
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+resultof\s+=|(fn|impl|mod|struct|enum|union|trait)\s+resultof)\b
returned
?
: Inconsistentrepogroup:crates case:yes max:400
\b((let|const|type|)\s+returned\s+=|(fn|impl|mod|struct|enum|union|trait)\s+returned)\b
Of these, only resultof
seems to be semantically descriptive and has some support. However, it has three major drawbacks:
Length: Compared to try
, it is 5 characters longer (see reasoning for fallible
).
Not a word: resultof
is in fact a concatenation of result
and of
.
This does not feel like a natural fit for Rust, as we tend to use a _
separator.
Furthermore, there are no current keywords in use that are concatenations of two word.
Result<T, E>
oriented: resultof
is too tied to Result<T, E>
and fits poorly with Option<T>
or other types that implement Try
.
All of the languages listed below have a try { .. } <handler_kw> { .. }
concept
(modulo layout syntax / braces) where <handler_kw>
is one of:
catch
, with
, except
, trap
, rescue
.
In total, these are 29 languages and they have massive ~80% dominance according to the TIOBE index and roughly the same with the PYPL index.
The syntactic form catch { .. }
seems quite rare and is,
together with trap
, rescue
, except
, only used for handlers.
However, the <kw> { .. }
expression we want to introduce is not a handler,
but rather the body of expression we wish to try
.
There are however a few languages where catch { .. }
is used for the fallible
part and not for the handler, these languages are:
However, the combined popularity of these langauges are not significant as
compared to that for try { .. }
.
None as of yet.