DEV Community

Cover image for Two Rust features that I miss in Other languages
YJDoc2
YJDoc2

Posted on • Updated on

Two Rust features that I miss in Other languages

Hello everyone!

Now-a-days I am writing code in several different languages : Rust, C , JavaScript, Python. While all of these have their own use cases , Rust is one of my favorite languages in which I enjoy writing code in.

I have been writing code in Rust for about 1.5 - 2 years now, and it is still just as fun (if not more) as it was when I started. Rust has many great features which makes it different from others, and all of them have their own pros and cons. However, there are some features in Rust which I miss when writing code in other languages.

In this post, I'll be talking about two such features : One which you might know and be familiar with, and one which you might not know directly. I think both of them are valuable not only for better code, but even from a developer's perspective, and helps me to think clearly and write better code.

Error Management

Now, I don't mean to say other languages do not have error handling. Its just that the way Rust does error handling has an elegance and simplicity , which I miss in other languages.

I truly feel that Rust has done a great job with error handling. Even if not everyone might agree with the particular way it is done , I still feel it is one of the best error handling ideology that I have seen.

Error handling in Rust gets divided into two distinct types of errors :

  • Recoverable/ "true" errors : This is the most common one, which is almost entirely supported by Result enum. These kinds of errors are something that you know might occur, and want to bubble up, or display to users.
  • Non-recoverable / "exceptions" : These are signified by panics and unwraps. These signify that some invariant is broken; or that some basic assumption which should have been true, is actually false. In such cases there is really no point on continuing, and crashing is a better option.

Most of other languages do not separate these two kinds.

  • Java, Python, JS only have Exceptions, a single mechanism which must be used to indicate both ; or we must have an error:true/false field in all return types, and caller must check before using the returned value.
  • C has errno . To start with, it is a global variable for the whole code, and there is not much support to attach custom error messages without doing something similar as returning an enum with error and data components.

Update : As some have pointed out in the comments, Java and scala has an Either type somewhat equivalent to Result in Rust, and Go has error wrapping. I missed them originally when writing, as I do not work with these languages much.


Rust separating these two kinds allows me to think on what possible errors might occur - all known possible errors are declared upfront along with the return type, and only unexpected errors can cause crashes at runtime.

Apart from that, I think Rust has beautiful syntax support, which makes it feel that error handling was a first-class thing, not just an after thought.

  • ? operator allows bubbling up errors in a clean way. No need to have only a top level catch, or do a catch-check-rethrow.
  • unwrap() calls indicate what are our base assumptions for the code.
  • unwrap_or and similar APIs on Result allows setting a default / sane fallback in clean way.

Because of such nice support, there are quite a few libraries which build on top of this and allows us to have more good things :

  • anyhow is one of the GOAT crates, allowing to bubble up many error types from a single function, and attach context to errors
  • eyre is a fork-extention of annyhow, and provides way to derive error types and reports for structs

consider the following which uses anyhow crate :

...
let result1 = function1().context('function 1 errored')?;
let result2 = function2(param1).with_context(|| format!('function 2 errored with param {param1}'))?;
let result3 = result1.update(result2).context('update with result2 failed')?;
...
Enter fullscreen mode Exit fullscreen mode

In this way we can attach context to each individual error at each step, without having to add any additional piece of code. To the best of my knowledge, there is no straightforward way of doing something similar in other languages.

Consider doing similar in JS : if we use Exceptions to indicate errors, we either have to wrap all three in single try-catch and lose the granularity of context ; or have a try-catch for each, attach context to the caught error and re-throw from the catch block. This quickly gets out of hand as the code grows and there are more possible points for errors.

#[must_use] Annotation

Even though #[must_use] is not directly a part of Rust syntax / language itself, I think this is one of its underrated parts.

#[must_use] does not modify the code's output in any way, however it provides a lint which can be very helpful for catching some easy-to-miss bugs.

When we annotate any type with this, values of those types must be used. Consider the Result type which is annotated with #[must_use]. If we call a function which returns a result, but do not use / capture that return value :

result_returning_function();
Enter fullscreen mode Exit fullscreen mode

we will get a compiler warning saying

warning: unused `Result` that must be used
Enter fullscreen mode Exit fullscreen mode

Thus, we can make sure all Results are acknowledged, and we do not accidentally miss any potential error.

This also beautifully integrates with Futures. As Futures must be polled for them to resolve, if we create a future, but do not await it, we get a warning such as

warning: unused implementer of `Future` that must be used
...
note: futures do nothing unless you `.await` or poll them
Enter fullscreen mode Exit fullscreen mode

Finally Rust stdlib itself uses this to warn us of some gotchas. For example wrapping_add and such methods are directly called on values, but do not modify those values, but instead return a new value. One can easily forget this, and assume that the original value is modified. This warning prevents us to miss it easily.

As far as I know, no other language has anything similar, even considering external linters. I miss this specifically in JS, where there are no ways to ensure async functions are (eventually) awaited when called. Because of that, I sometimes miss awaiting a single async function call in async context, and that single function runs asynchronously, while rest do not.

Having the #[must_use] or equivalent would make catching such errors much easier.


Thank you for reading! Are there any other features of Rust you miss when coding in other languages? Or Any features of other languages that you miss when coding in Rust? Feel free to let me know your thoughts in the comments :)

Top comments (16)

Collapse
 
nsengupta profile image
Nirmalya Sengupta

Good article.

#[must_use] is a great aid. I agree with you.

However, we can use an exact equivalent of Result in Java/Scala. It is called 'Either'. Perhaps, you already know about it. When instantiated, a value of Either can be of either the 'L' type - conventionally, indicating an error - or the 'R' type, which is the value, we are otherwise looking for.

Scala has built in Either type. In Java 8+ (I have used till Java 17), by using the fantastic 'vavr' library, 'Either' can be put to use, including the pattern-matching idioms, map(), flatMap() etc, much like Rust lets us.

I don't know Kotlin, but I assume Kotlin has something similar. Experts can comment.

Collapse
 
yjdoc2 profile image
YJDoc2

Hey, thanks for the comment! I have updated the article to mention this. I don't work much with Java/Scala so didn't know that. Does the Either type also have similar ergonomics as rust's Result? I know that types like Rust's enum (I think they are called additive data types?) exist in several other languages, but I didn't knew those have similar ergonomics as Rust's results have.

Collapse
 
nsengupta profile image
Nirmalya Sengupta

As for ergonomics, @omidrad has already elaborated. the difference between Rust and Java. I differ slightly from his PoV and I have stated that in my response to him.

Collapse
 
swandog profile image
Ken Swanson

Scala also has the Try type, which is the more exact analogue to a Rust Result, and it's had it since 2.10.

Some older Scala code uses Either since it came out first, but most of the Scala code I've read and written (admittedly, it's been a few years) use Try[T] over Either[T, Error].

Collapse
 
omidrad profile image
Omid Rad

But that's different.

When you are in the ecosystem and use some dependencies, some deps use Either and some use exception. It makes things more complicated and ugly and at some point you need to handle both at the same time!

Either does exist in Rust (as library, and removed long time ago from the std lib), but IMO to use it as a way to handle errors is lame.

On top of these, there is no difference between L and R in Either, there is no Ok and Err. A library may see L as Err and another one R as Err. Also what about ? in Rust?

Anyway, they are not the same.

Collapse
 
nsengupta profile image
Nirmalya Sengupta • Edited

Not same, of course. But equivalent. :-)

I have steadfastly - one may say determinedly - stayed away from all checked exceptions for last few years in all my Java applications. Wherever pressed, I have wrapped the exception-throwing methods into Either returning methods.

Not easy always, as you have correctly pointed out - but demonstrably doable.

The other point you raise about L and R being not a fixed type, you are right. The convention of course, is that L is for errors and R is for acceptable values. However, nothing stops me from making L a subtype of an master Error type. The compiler does the rest.

Thread Thread
 
omidrad profile image
Omid Rad

I would say they are not equivalent also.

That's a misuse of Either to handle errors on other languages. Or let's say the best way they can do so. There are a lot around Result in Rust. There is nothing specific for error handling for Either.

The only common thing is that, both Result and Either are two-variant enums! (I don't know how exactly they called them in Java)

It's like people saying Java also has Option. But that's different between when the whole ecosystem only has Option or there is a possibility of using Options. They are not the same or equivalent.

We don't have these kinds of inconsistency, because it's a younger language. But I hope we don't have it in future also.

Thread Thread
 
swandog profile image
Ken Swanson

Scala Eithers are not exactly as blank as that, because they are "right-biased"; meaning that, in all comprehensive operations, it considers Right values to be present and Left values to not be. This is why you can chain flatMaps of Eithers together without having to designate what the types mean; for a Scala Either, the Right is always the default.

It's also why an Either with an error in the Left type is a simple implementation of the Result type; you map/flatMap over them, and if you end up with a Left, you got an error.

That said, as I said elsewhere Scala also has the Try type which I think works better, if for no other reason than the semantics around it more clearly indicate you're dealing with errors.

(Scala doesn't have the ? operator, it's true, because IIRC the style prefers keeping values inside the container and applying transformations to it, instead of extracting the value or returning early. YMMV)

Thread Thread
 
omidrad profile image
Omid Rad • Edited

Scala doesn't have anything related to errors for its Either. Does it have is_err for example (sure you can check if the left is there or right)? Does it have map_err? Does it have expect? Does it have or_X functions? Does it have unwrap functions? and so on...

There can be a library which can add these functionalities to the language. But as I mentioned many times, it's not the same!
When you can use different ways of having errors (in this case) and you don't follow the language standard/default way. It creates inconsistency in the ecosystem, specially as a library developer or when you use external libraries!

If I was a Java/Scala developer, and wanted to develop a library, I would never use Either to handle/expose errors.

But Try is something similar to Rust's Result. The only bad part is that now devs need to handle Either, Try and Exceptions (and maybe more).

Thread Thread
 
nsengupta profile image
Nirmalya Sengupta

I am not into splitting hairs on semantic niceties! :-)

A concept exists in Scala / Java ( In Elixir and Haskell too, my FP guru friends tell me), to indicate that a function can return either of two possibilities but never, both.

If a function can return two different types of SUCCESS indicators, and one decides to use Either for that, it is fine! But, that's not what the common sense dictates.

The equivalence that I pointed out was about a facility in the type system to disassociate an ERROR from an ACCEPTABLE value that a function returns: much more easier to deal with, say errno of my C days or random Exceptions thrown in a JVM-based code. That's about it.

You are right, Rust has the advantage of being younger than many other languages and is offering a cleaner way to disassociate ERROR values from ACCEPTABLE values. Compiler's watchful eyes point out any inconsistencies earlier in the cycle. That helps a lot, too.

Thread Thread
 
nsengupta profile image
Nirmalya Sengupta • Edited

It's also why an Either with an error in the Left type is a simple implementation of the Result type; you map/flatMap over them, and if you end up with a Left, you got an error.

My point too.

And, there is a reason why Try in Scala / Java ( vavr gives the equivalent Try type in Java), has methods like toEither() and toOption().

Collapse
 
val_baca profile image
Valentin Baca

I haven't tried it, but it looks like Arrow is the Kotlin FP batteries included library and has Either (Kotlin can also just use vavr afaik)

arrow-kt.io/learn/typed-errors/eit...

Collapse
 
pkraszewski profile image
Paweł Kraszewski

there is no straightforward way of doing something similar in other languages.

With all my hate to if err!=nil{}, Go has error wrapping:

var criticalError = errors.New("Serious error")
// ...
wrapped := fmt.Errorf("...%w...",criticalError,...)
// ...
parent := errors.Unwrap(wrapped)

Enter fullscreen mode Exit fullscreen mode

See rollbar.com/blog/golang-wrap-and-u...

Collapse
 
yjdoc2 profile image
YJDoc2

Hey, I have updated the article to mention this. I had only seen the err != nil kind of error handling, and haven;t worked with go much, so didn't know that.

I checked the link, but I'm not sure. Does the wrapping-unwrapping provide similar ergonomics as Rust Results too?

Collapse
 
pkraszewski profile image
Paweł Kraszewski • Edited

Does the wrapping-unwrapping provide similar ergonomics as Rust Results too?

Hell not. I hate it not without a reason. I still find Rust's error handling superior to all the other languages I use(d).

Collapse
 
omidrad profile image
Omid Rad

Also when you are in the ecosystem and use some dependencies, some deps use unwrapped version and some use wrapped version. It makes things more complicated!