Categories > Coding > Rust >

Rust Best Practices 2: Error and Null handling

Posts: 15

Threads: 2

Joined: Feb, 2023

Reputation: 4


Error and null handling are two core concepts in any programming language; you can't be sure that everything will be there and working the way you want. In this post, we'll dive into how Rust™ handles them.


First, errors:


Languages like C tend to use error codes:


Error codes are quite a bad way to handle errors; you can't attach any extra data but the status code itself, you are allowed to ignore the error, and you can't use the integers that the macros occupy as successful return values as the macros have occupied their meaning.


In my previous post, I briefly introduced the Result type in Rust; it is an enum which can be in one of the Ok state (success) or the Err state (failure). The enum is generic over two types, T and E, which attach data to the tuple variants Ok and Err, respectively. The type looks like this:


Let's see how our example looks in Rust:


Note 1: neither of these examples reflects the reality of HTTP and networking in C or Rust; they're just contrived examples for wanting a number to be within a range.


This example is quite basic, but we can see the night and day difference between Rust and C, Err let us attach data to describe the error better, and we used it with yet another enum to make valid only the states of being too large or too small, unlike the C version which could accept any integer.


Let's keep exploring error handling in Rust. We looked at returning the error from main and letting Rust handle displaying the error when it occurs. This approach is used commonly in I/O code, combined with something we will cover soon: the "?" operator. The second technique used an inherent method on the Result type: "expect". Expect has the same behaviour as another method called "unwrap", which returns the T "wrapped" in Ok(T) or panics with the Err(E). What "expect" does differently is that it accepts an error message.


Here, our T (as in Ok(T)) for "send_http_code" was (), the unit type; having unit as the Ok type means that succeeding yields no information other than the success itself. Let's explore an example where the success case has some data we want and how we can handle getting at those data using a real-life situation from a crate I wrote for the LUNIR project called "lifering".


Our problem is as follows:

The most widely accepted and used standard for floating-point numbers is IEEE 754. Within the standard, it is detailed that NaN (Not a Number) can never be considered equal to anything. NaN also has many different bit patterns, so a simple bitwise comparison would fail. As such, floating-point types do not implement the Eq trait in Rust and only implement the PartialEq trait. The Eq trait is a marker trait which asserts that comparisons:


1. reflexive: a == a must hold (FAILED)

2. symmetric: a == b must imply b == a 

3. transitive: a == b and b == c must imply a == c 


To use a type as a key type in a HashMap, it must implement the Hash trait, which requires the type to be Eq. The result is that you cannot use floats as the keys of a HashMap, which is annoying for people who need that. So, I created the "lifering" crate to remediate the issue. I store the float as its three components in a struct; they are the sign, mantissa, and exponent:


I used the Float trait of the "num" crate to be generic over f64 and f32, as the type was large enough to cover both. The constructor is as follows:


You might notice a few things; the first and most obvious is the "FloatWrap" type; this is purely due to a limitation of Rust. The second thing is the "NanError" type, "NanError" is a unit struct (a struct that behaves like () [zero-sized, inhabited by one value] but has a name) which I use because I can only fail on the one case of receiving a NaN. "NanError" is defined as follows:


Again we derive Debug for the type so it can be displayed. The TryFrom implementation goes as follows:


You can see that "Self" takes on two meanings here:


1. The type that TryFrom is being implemented on.

2. This implementation of TryFrom in specific.


We can see those two meanings together in the return value of "try_from". The first "Self" is the type — "FloatingPointComponents", while the second "Self" is the implementation, and we get at the associated type "Error", which we specified to be "NanError". Inside the body, we check if the float is a NaN, and if it is, we return an Err(NanError); else, we decode the float into its integer components and store them in a "FloatingPointComponents" and return them in an Ok.


Great, now let's say we're consumers of this crate; what do we do? Lifering defines a simple macro called "lifering" due to the verbosity of manually creating a "FloatingPointComponents". Let's see how it handles the error:


Aha, it unwraps inside, which is excellent for conciseness but bad for custom handling. Let's explore how we can customly handle errors from the "Lifering" crate.


First, let's look at the "?" operator. If the value succeeds, the operator will unwrap it, and if it's an error, the operator will return it. Let's see an example:


This is nice and concise. It functions like an unwrap, too, but what if we want to use our own error message? Pattern matching:


Patterns can get arbitrarily complex and are valid in many more places. You can read further about this here:


We can use several mechanisms of pattern matching to achieve this same goal of panicking with a custom message, first, "if-let":


We can use "if-let" as a statement like this:


Or as an expression like this:


This is a bit more verbose than just using a match, so let's move on to our second technique, match:


Relatively recently introduced to Rust are refutable patterns with "let .. else", our final technique:


Next, nulls:


Much of the same applies to how Rust handles no value with the Option type, so showing the signature is enough for you to understand how to use it. It also implements Try, and like Result, is in the standard prelude (it and its variants are in scope by default).


I think this post has become big enough already, so I will push iterators to Rust™ Best Practices 3.




Note: I've changed the image tool because is garbage.

Note: The ™ symbol is being used for now as the Rust Foundation is finally securing its trademarks. They have not been registered as of the writing of this post.


Don't fight or I'll lock. The next post will be about Iterators.

  • 0

Rust is the future and the future is now


Web Developer


Posts: 1298

Threads: 40

Joined: Jul, 2021

Reputation: 67


I think I'm so lazy...for read this.

  • 0

I'm not lazy, I'm just highly motivated to do nothing. #I💚Dogs.

Posts: 531

Threads: 20

Joined: Nov, 2022

Reputation: 44


very useful post, although large, it's of really good quality, thanks!

  • 0

Posts: 2

Threads: 0

Joined: Jun, 2023

Reputation: 0


very useful post


  • 0

Posts: 1430

Threads: 71

Joined: May, 2022

Reputation: 20


or just.. dont use rust


Alawrpar 44 Reputation


not an option /chars

  • 0

  • 0

Did I mention I use arch btw?

Users viewing this thread:

( Members: 0, Guests: 1, Total: 1 )