DEV Community

Cover image for Rust Tutorial 3: Handling Errors and Other Concepts
Khair Alanam
Khair Alanam

Posted on

Rust Tutorial 3: Handling Errors and Other Concepts

Reading time: 11 minutes

Welcome to the Rust Tutorial 3!

This will be a short tutorial and is a continuation of the previous tutorial.

In this tutorial, we will handle errors in our guessing game and also learn some more concepts on the way.

So let's get started!


const keyword

Just like the let keyword, const is used to declare variables in Rust that will never be changed through out the program.

One key thing you have to know about const variables is that, we cannot mutate these variables, even if you use the mut keyword. Using the mut keyword in a const variable will throw an error.

Also, it's a convention in Rust to use upper snake case names for const variables.

Let's just use an example:

fn main() {
    const SOME_NUMBER: i32 = 100_000_000;
}
Enter fullscreen mode Exit fullscreen mode

Fun fact: you can use underscores in an integer if the given number is huge for better readability.

99% of the time, you will be using the let keyword. So just use that. If you are really sure that your variable doesn't change at all in the main program execution, then you may use const.


match case

The match case statement in Rust is very similar to the switch case statements you see in other programming languages. A match case is a clean way of writing multiple if statement that use the same variable to check for some value. An example looks like this:

fn main() {
    let age: i32 = 30;

    match age {
        1 => println!("One!"),
        2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
        13..=19 => println!("A teen"),
        _ => println!("Just some number"),
    }
}
Enter fullscreen mode Exit fullscreen mode

In the match statement, each value in the LHS of the => expression represents the cases of the match statement, separated by commas. For instance, If age is 1, then we print "One!".

We can chain multiple cases using the pipe | expression if many values give the same output. You can also use a range of numbers as a case.

In case none of the numbers match, we use underscore _ to define a default case.


match case with variable assignments

Let's take a look at this program:

fn main() {
    let age: i32 = 30;
    let res: &str;

    match age {
        1..=10 => res = "Child",
        11..=18 => res = "Teen",
        20..=30 => res = "Young Adult",
        _ => res = "Adult",
    }

    println!("{}", res);
}
Enter fullscreen mode Exit fullscreen mode

This program prints out the group type based on age using match case. But notice that in every case, I am assigning a string to the variable res. You can see that it seems repetitive.

Well there's a way to make it DRY. Checkout this one:

fn main() {
    let age: i32 = 30;

    let res: &str = match age {
        1..=10 => "Child",
        11..=18 => "Teen",
        20..=30 => "Young Adult",
        _ => "Adult",
    };

    println!("{}", res);  // prints "Young Adult"
}
Enter fullscreen mode Exit fullscreen mode

All I did was declare the res variable and assign it to the match case statement based on the age value.

Seems pretty intuitive. Right?


Block statements and Block scope

There are lot of syntactic sugar in Rust that you wouldn't see in other programming languages. Though all the programming languages has some kind of syntactic sugar, Rust has a unique way of handling them. Block statements are one of them.

Block statements are pretty useful when you want to assign a value to some variable "once" (thus not needing a separate function) but requires some complex calculations to get that value.

A major advantage of block statements is that any variable declared inside the block statement are scoped to that block only. This helps in keeping the program consistent and prevent the program from any kind of scope errors.

Let's take an example:

fn main() {
    let age: i32 = 30;

    {
        let age: i32 = 24;
        println!("Block Age: {}", age);
    }

    println!("Global Age: {}", age);
}
Enter fullscreen mode Exit fullscreen mode

Running the code will give this output:

Block Age: 24
Global Age: 30
Enter fullscreen mode Exit fullscreen mode

You would expect that the age would change to 24 outside the block due to shadowing, but that's not what happened.

Due to block scope, the age variable declared inside the block will not affect anything outside the block.

Except, if you are mutating the age variable. Like this:

fn main() {
    let mut age: i32 = 30;

    {
        age += 24;
        println!("Block Age: {}", age);
    }

    println!("Global Age: {}", age);
}
Enter fullscreen mode Exit fullscreen mode

This will print:

Block Age: 54
Global Age: 54
Enter fullscreen mode Exit fullscreen mode

So, yea. If the variable is being declared as mutable, then block statements can change the variables outside the block scope.

Now let's get to an interesting part.


Assigning block statements to the variables.

Let's write this code:

fn main() {
    let some_num: i32 = {
        let x: i32 = 45;
        let y: i32 = 34;
        x + y
    };

    println!("{}", some_num);
}
Enter fullscreen mode Exit fullscreen mode

In this code:

  • I declared a variable called some_num.
  • Then I declared a block statement in which I declared two local variables x and y and assigned some values.
  • Then I add them x + y and assign the sum to some_num.

This will print out "79" in the console.

Now, you might be confused with the x + y term that seems out of place compared to the rest of code. Basically what's happening is that after I add x and y, since there's no other code remaining, The block will return this value, i.e. x + y, and assign it to some_num.

Also, notice that I didn't put a semi-colon after x + y. This is because if you used a semi-colon after x + y then the value cannot get assigned to the variable and will return some empty block.


Some more loops!

Let's get back to the loops! In the previous tutorial, I talked about the loop expression, the simplest of the loops. Now let's learn the for and while loop.

A while loop in Rust looks like this:

fn main() {
    let mut i: i32 = 0;

    while i < 5 {
        println!("{}", i);
        i += 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

A for loop in Rust looks like this:

fn main() {
    for i in 1..=5 {
        println!("{}", i);
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice we have used a range in a for loop.


Making a Better Guessing Game

In the previous tutorial, we have made our guessing game. But you might have noticed that we can guess as many times as you want and the game wouldn't end.

So let's implement a guess limiter to our game.

To do that, we can simply replace the loop expression with a while loop expression. For our game, we can let the player have at maximum 5 guesses.

Here's how the code will look like:

use std::io;
use rand::Rng;

fn main() {
    let secret_number: i32 = rand::thread_rng().gen_range(1..101);
    let mut i: i32 = 0;

    while i < 5 {
        println!("Guess {}", i + 1);
        println!("Guess the number between 1 and 100: ");
        let mut user_input: String = String::new();

        io::stdin().read_line(&mut user_input).expect("Some Input Error.");

        let user_input: i32 = user_input.trim().parse().expect("Not an integer");

        if user_input < secret_number {
            println!("Too small!");
        } else if user_input > secret_number {
            println!("Too big!");
        } else {
            break;
        }

        i += 1;
    }

    if i == 5 {
        println!("You have lost! The number was {}!", secret_number);
    } else {
        println!("You guessed it! It's {}!", secret_number);
    }
}
Enter fullscreen mode Exit fullscreen mode

The changes that I made here are:

  • I declared a mutable variable i with initial value as 0.
  • I replaced the loop with a while loop when i < 5 (5 guesses).
  • I added a print statement to keep track of the guesses.
  • Once the while loop is over, whether normally or using break statement, it will go through an if block (outside the while loop) to see if i is equal to 5 or not and then set the print statements for each condition.

You can now play the game and you will notice you will only have five guesses to guess the number.


Now let's play the game, but this time let's input some string instead of an integer as our guess.

Let's run the cargo run command and see the output:

Rust error output

We get an error since we input a string and not an integer. We have to handle these errors in our code.

Let's take a look at our code:

Rust code analysis

If you observe, the line of code pointed by the arrow is where we are getting the error. We need to modify this line to handle our errors.

What we can do is, if the user inputs a valid input, then it's all okay. But if the user inputs an invalid input, we can notify the user that it's an invalid input and then restart the loop.

We can do this using match case statement for the user_input variable.

But what will be the variable which we will match the values to?

That will be the user_input.trim().parse().

We know that this function will either return a valid result or an error.

We can verify the return value of the function by using these two enums:

  • Ok() to see if it's a valid result, and
  • Err() to see if it's an error.

These two enums will be our cases for matching user_input.trim().parse() and then assign that to the user_input.

Let's modify that one line of code to this:

let user_input: i32 = match user_input.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("Invalid Input! Try again!");
                continue;
            },
        };
Enter fullscreen mode Exit fullscreen mode

In this code, The first case is when the result is valid. We first check whether the user_input.trim().parse() returns an integer, say num. The Ok(num) will verify if it's a valid result.

The second case is when user_input.trim().parse() returns an Error object. Since, we will check for any kind of errors, we use an underscore _ inside Err() to verify for any errors.

Also, notice that I removed expect() function since we are already handling the errors in a better way.


Now let's run the code and see the output:

Final Rust output

As you can notice, I have input some invalid inputs to test the game and it works!

Great job! You have made a better guessing game!


I hope you have learnt a lot in this short tutorial. As I have said, I would have included this in the previous tutorial but that one was getting too long and hence, I decided to write a new one just for this.

In the next tutorial, we will make a simple calculator using Rust.

Until next time, have an awesome day ahead!

GitHub Repo: https://github.com/khairalanam/better-rust-guessing-game

If you like whatever I write here, follow me on Devto and check out my socials:

LinkedIn: https://www.linkedin.com/in/khair-alanam-b27b69221/
Twitter: https://www.twitter.com/khair_alanam
GitHub: https://github.com/khairalanam

Top comments (0)