Rust Error Troubleshooting Comprehensive Guide to Memory Management

Error Handling In Rust

An error is an unrehearsed behavior or event in a program that will generate an unwanted output.

In Rust Program, errors are two categories:

  • Unrecoverable Errors
  • Recoverable Errors

Unrecoverable Errors in Rust Programming

Unrecoverable errors are errors to which a program stops its performance. As the name suspects, we cannot restore from unrecoverable errors.

this errors are known as panic and can be triggered apparently by calling the panic! macro.

Now let’s look at an example that uses the panic! macro.

Example 1: Unrecoverable Errors with panic! Macro In Rust

fn main() {
    println!("Hello, World!");

    // Explicitly exit the program with an unrecoverable error
    panic!("Crash");
}

Output :

Hello, World!
thread 'main' panicked at 'Crash', src/main.rs:5:5

Hither, the call to the panic! macro reasons an unrecoverable error.

thread 'main' panicked at 'Crash', src/main.rs:5:5

Follow that the program still runs the evolutions above panic! macro. We can only see Hello, World! printed to the system at first the error message

The panic! macro receives in an error message as an argument.

Example 2: Unrecoverable Errors In Rust

Unrecoverable errors are further triggered by receiving an action that might cause our code to panic. For example- appreciating an array past its index will cause a panic.

fn main() {
    let numbers = [1, 2 ,3];

    println!("unknown index value = {}", numbers[3]);
}

Error

error: this operation will panic at runtime
 --> src/main.rs:4:42
  |
4 |     println!("unknown index value = {}", numbers[3]);
  |                                          ^^^^^^^^^^ index out of bounds: the length is 3 but the index is 3
  |

Rust program stops us from making the program because it knows the activities will panic at runtime

The layout numbers does not have a value at index 3 i.e. numbers[3].

Recoverable Errors In Rust

Recoverable errors are errors that won’t stop a program from acting. Most errors are recoverable and we do easily receive action based on the type of error.

For example-if you try to open a file which doesn’t exist, you can make the file instead of stopping the performance of the program or departing the program with a panic.

Now see at an example-

use std::fs::File;

fn main() {
  let data_result = File::open("data.txt");

  // using match for Result type
  let data_file = match data_result {
      Ok(file) => file,
      Err(error) => panic!("Problem opening the data file: {:?}", error),
  };

  println!("Data file", data_file);
}

If the data.txt file subsist, the output is here:

Data file: File { fd: 3, path: "/playground/data.txt", read: true, write: false }

If the data.txt file doesn’t subsist, the output is here:

thread 'main' panicked at 'Problem opening the data file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23

The Result Enum In Rust

In the upon example, return type of the File::open('data.txt') is a Result<T, E>.

The Result<T, E> type returns other a value or an error in Rust Program. It’s an enum type with two probable variants.

  • Ok(T) → operation repeated with value T
  • Err(E) → operation unsuccessful with an error E

The most primary way to see if a Result enum has a value or error is to use pattern become like with a match evolution.

// data_file is a Result<T, E>
match data_result {
    Ok(file) => file,
    Err(error) => panic!("Problem opening the data file: {:?}", error),
 };

Whereas the result is Ok, these code will return the file, and Whereas the result is Err, these will return a panic!.

The Option Enum In Rust

The Option type or Option<T> type is an enum type as like the Result with two probable variants.

* `None` → to reveal failure with no value
* `Some(T)` → a value type `T`

Now see an example-

fn main() {
    let text = "Hello, World!";
    
    let character_option = text.chars().nth(15);
    
    // using match for Option type
    let character = match character_option {
        None => "empty".to_string(),
        Some(c) => c.to_string()
    };
    
    println!("Character at index 15 is {}", character);
}

Output :

Character at index 15 is empty

The method text.chars().nth(15) interchange an Option<String>. Now, to be the value out of the Option, you use a match evolution.

The example upon, the 15th index of the string text doesn’t subsist. these, the Option type returns a None as matches the "empty" string.

None => "empty".to_string() 

If you were to be the 11th index of the string text, the Option enum will return Some(c), Here c is the letter in the 11th index.

Let’s update the upon example to discovery out the 11th index in the string.

fn main() {
    let text = "Hello, World!";
    
    let character_option = text.chars().nth(11);
    
    // using match for Option type
    let character = match character_option {
        None => "empty".to_string(),
        Some(c) => c.to_string()
    };
    
    println!("Character at index 11 is {}", character);
}

Output :

Character at index 11 is d

Rust unwrap() and expect() In Rust

The unwrap() and expect() appropriateness methods that work with Option and Result types in Rust Program.

The unwrap() Method In Rust

Unwrap in Rust Program returns the consequence of the operation for Option and Result enums. If unwrap appointments an error Err or None, it’s will panic and stop the program performance.

Unwrap method are defined on both Option and Result type.

An Option enum type can be moved about by using the match evolution as well as unwrap().

Example: Using the match Expression In Rust

// function to find a user by their username which returns an Option type
fn get_user(username: &str) -> Option<&str> {
    if username.is_empty() {
        return None;
    }

    return Some(username);
}

fn main() {
    // returns an Option
    let user_option = get_user("Hari");

    // use of match expression to get the result out of Option
    let result = match user_option {
        Some(user) => user,
        None => "not found!",
    };

    // print the result
    println!("user = {:?}", result);
}

Output :

user = "Hari"

Now, you have a get_user function that reports an Option type. It can other return Some(&str) or None.

Here, it’s program can use the unwrap() method to be release of the match evolution which is a little wordy.

Now we use unwrap() in the upon example.

Example: Using unwrap() In Rust

// function to find a user by their username which return an Option enum
fn get_user(username: &str) -> Option<&str> {
    if username.is_empty() {
        return None;
    }

    return Some(username);
}

fn main() {
    // use of unwrap method to get the result of Option enum from get_user function
    let result = get_user("Hari").unwrap();

    // print the result
    println!("user = {:?}", result);
}

Output :

user = "Hari"

Both the match evolution and unwrap() gives us the equivalent output. The only inequality existence that unwrap() will panic if the return value is a None.

The expect() Method In Rust

expect() is very like to unwrap() with the collation of a custom panic message as an contention.

The expect() method is defined on both Option and Result type.

Now update the upon example to use expect() instead of unwrap().

// function to find a user by their username which return an Option enum
fn get_user(username: &str) -> Option<&str> {
    if username.is_empty() {
        return None;
    }

    return Some(username);
}

fn main() {
    // use of expect method to get the result of Option enum from get_user function
    let result = get_user("").expect("fetch user");

    // print the result
    println!("user = {:?}", result);
}

Output :

thread 'main' panicked at 'fetch user', src/main.rs:12:31

Below, we use the expect() with a panic message as the contention.

expect() and unwrap() will generate the same result if there’s no probability of Option returning None and Result returning Err.

The Question Mark (?) Operator In Rust

The question mark (?) operator is a shorthand for revolving the Result. It’s can only be practical to Result<T, E> and Option<T> type.

Now we apply ? to Result<T, E> type:

  • Supposing the value is Err(e), it returns an Err() instantly
  • Supposing the value is Ok(x), it unwraps and returns x

Now look at an example-

use std::num::ParseIntError;

// Function to parse an integer
fn parse_int() -> Result<i32, ParseIntError> {
    // Example of ? where value is unwrapped
    let x: i32 = "12".parse()?; // x = 12
    
    // Example of ? where error is returned
    let y: i32 = "12a".parse()?; // returns an Err() immediately
    
    Ok(x + y) // Doesn't reach this line
}

fn main() {
    let res = parse_int();

    println!("{:?}", res);
}

Output :

Err(ParseIntError { kind: InvalidDigit })

The way, error stirring in the function is attenuate to a single line of code, building it cleaner and simple to read.

Ownership In Rust

Rust Program comprise an ownership appointments to manage the memory of our program. proprietary is a set of rules that confirm memory safety in Rust programs.

Variable Scope in Rust Program

A scope is a code block among the program for as a variable is valid. The scope of a variable identify its ownership.

For Example-

// `name` is invalid and cannot be used here because it's not yet declared
{ // code block starts here
    let name = String::from("Ram Nepali");   // `name` is valid from this point forward
    
    // do stuff with `name`
} // code block ends
// this scope ends, `name` is no longer valid and cannot be used

Above the variable name is only obtainable inside the code block, i.e., among the curly braces {}. You cannot use the name variable beside the closing curly brace.

Ownership Rules in Rust Program

Rust Program has some ownership rules. placement these rules in mind as we work by some examples-

  • Every value in Rust Program has an owner.
  • This can only be one owner contemporary.
  • When the owner passage out of scope, the value will be exuded.

Data Move in Rust program

Once in a way, we strength not want a variable to be exuded at the end of the scope. Instead, we want to disposal ownership of an item from one binding (variable) to different.

Nows an example to understand data stroll and ownership rules in Rust Program.

fn main() {
    // owner of the String value
    // rule no. 1 
    let fruit1 = String::from("Banana");
    
    // ownership moves to another variable
    // only one owner at a time
    // rule no. 2
    let fruit2 = fruit1;
    
    // cannot print variable fruit1 because ownership has moved
    // error, out of scope, value is dropped
    // rule no. 3
    // println!("fruit1 = {}", fruit1);
    
    // print value of fruit2 on the screen
    println!("fruit2 = {}", fruit2);
}

Output :

fruit2 = Banana

Now, look into this example in detail, particularly these two lines of code

let fruit1 = String::from("Banana");
let fruit2 = fruit1;

A String store data both of the stack and the heap. That’s mean that when we bind a String to a variable fruit1, the memory deputation looks like this below-

Screenshot-2024-01-19-021154

A String maintain a pointer to the memory that maintain the content of the string, a length, and a receptivity in the stack. The rick on the right hand side of the graph holds the contents of the String.

When we assign fruit1 to fruit2, this is how the memory deputation looks like-

Screenshot-2024-01-19-021701

Copy Data in Rust program

primordial types like Integers, Floats and Booleans don’t ensue the ownership rules. this types have a known size at compose time and are stored absolutely on the stack, so cabbage of the authentic values are quick to make. For example-

fn main() {
    let x = 11;
    
    // copies data from x to y
    // ownership rules are not applied here 
    let y = x;

    println!("x = {}, y = {}", x, y);
}

Output :

x = 11, y = 11

Below, x variable can be used after, unlike a move without disquieting about ownership, even though y is imposed to x.

Functions Ownership in Rust

Momentary a variable to a function will step or copy, just as an employment. Stack-only types will copy the data when acquired into a function. stack data types will move the ownership of the variable to the function.

1. Passing String to a function In Rust

fn main() {
    let fruit = String::from("Apple");  // fruit comes into scope
    
    // ownership of fruit moves into the function
    print_fruit(fruit);
    
    // fruit is moved to the function so is no longer available here
    // error
    // println!("fruit = {}", fruit);
}

fn print_fruit(str: String) {   // str comes into scope
    println!("str = {}", str);
}   // str goes out of scope and is dropped, plus memory is freed

Output :

str = Apple

The value of the fruit variable is driven into the function print_fruit() forasmuch as String type uses stack memory

2. Passing Integer to a function In Rust

fn main() {
    // number comes into scope
    let number = 10;
    
    // value of the number is copied into the function
    print_number(number);
    
    // number variable can be used here
    println!("number = {}", number);
}

fn print_number(value: i32) { // value comes into scope
    println!("value = {}", value);
}   // value goes out of scope

Output :

value = 10
number = 10

The value of the number variable is followed among the function print_number() because the i32 (integer) type exercise stack memory

References and Borrowing In Rust

References in Rust programs assume us to point to a resource (value) without identify it. That means the original owner of the resource remnant the same.

References are subsidiary when passing values to a function that we do not want to alternative the ownership of. Making a reference is known as borrowing in Rust.

Understanding References in Rust Program

Now look an example to learn about references in Rust Program

fn main() {
    let str = String::from("Hello, World!");
    
    // Call function with reference String value
    let len = calculate_length(&str);

    println!("The length of '{}' is {}.", str, len);
}

// Function to calculate length of a string
// It takes a reference of a String as an argument
fn calculate_length(s: &String) -> usize {
    s.len()
}

Output :

The length of 'Hello, World!' is 13.

In the upon example, we identify a function called calculate_length() which accepts a &String type as an contention.

The significant part here is that s is a reference to a String and it doesn’t accept ownership of the original value of String.

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
}

The function call looks like as:

let str = String::from("Hello, World!");

let len = calculate_length(&str);

The &str syntax when calling the function lets us make a reference that mention to the value of str but does not own it.

Modifying a Reference in Rust Program

By inability a reference is constantly immutable. Only, we can conduct the &mut keyword to create a reference mutable.

Now, For Example-

fn main() {
    let mut str = String::from("Hello");
    
    // before modifying the string
    println!("Before: str = {}", str);

    // pass a mutable string when calling the function
    change(&mut str);
    
    // after modifying the string
    println!("After: str = {}", str);
}

fn change(s: &mut String) {
    // push a string to the mutable reference variable
    s.push_str(", World!");
}

Output :

Before: str = Hello
After: str = Hello, World!

You set the variable str to be mutable. After you create a mutable mention with &mut str, and call the change() function with a mutable mention s: &mut String.

This make allowance for the change() function to modify the value it sharpen. Inside the change() function, we push a string with s.push_str(“, World!“) to the reference string.

Previous
Exploring the Core Features of Rust Standard Library
Next
Rust Modules: A Comprehensive Guide to Packages and Best Practices