From the course: Debugging Rust Code with AI

Common debugging challenges in Rust

From the course: Debugging Rust Code with AI

Common debugging challenges in Rust

- [Instructor] Hey, Rust enthusiast. In our last video, we tackled error handling. Now let's conquer some debugging challenges. Ownerships, lifetimes, and stack traces await us. Ready? Like this quarter on the slide, you will master this with confidence. Let's dive in. Ownership is at the heart of Rust safety guarantees, but it can also be one of the trickiest concepts to master. Let's switch over to IDE, and break it down with a simple example. Here, I have a simple Rust snippet that highlights a common pitfall, trying to modify a value while it's borrowed. Rust borrow checkup prevents this to ensure memory safety. Let's see what happens. So as we see in the code, we are creating a mutable string original with the value Hello. The mut keyword just means we are allowed to change it later. Now we create an immutable reference to the original call borrowed. That means we can read the string through borrowed, but we can't change it. And here's where Rust borrowing rules kick in. You can have multiple immutable references, do something at once, that's not an issue, but you can't modify the original while there's still an active immutable reference. And if you see the line over here, line number four, we try to change original by adding World to it, but since borrowed is still hanging around, Rust throws an error. Now we see this popup, "Cannot borrow original as mutable because it is also borrowed as immutable. Mutable borrow occur here." As we see in the IDE, this line original.push str throws a error, why? Because we're trying to change original while borrowed and immutable reference are still in scope. Rust borrow checker doesn't allow that. No mixing reads and writes at the same time, that's how Rust keeps things safe before your code even runs. Finally, we print out borrowed here, which just holds still Hello. Now if you're from Java or Python background, this would run fine but might cause memory issues. In Rust, it won't even compile, preventing your hidden bugs. So what did we learn? So the key is scope. If we ensure the borrowed reference is used and dropped before modifying, in this case, original, Rust is happy. So let's modify a code to respect these borrowing rules. Here's the improved version. Notice how we introduce a scope around the borrowing. That way, once the reference is used, it's dropped and we can safely modify original string later. Now let's run it and check the output. As we can see, we no longer see the error message. Let's run the code. And there you have it, the first println inside the scope prints Hello as expected. Once the borrowed reference goes out of scope, Russ lets us modify original and we successfully append World. Problem solved. So what's the key takeaway? Rust borrowing rule keeps our code safe, but with a little scope magic, we can work around these restrictions. But what if the problem isn't about when something is used, but how long it lives? That's when things get trickier and where lifetime comes in. Let's look at an example where we return a reference from a function. We will use a User struct and see how Rust lifetime catches a bug before it even runs. All right, so here's what the code is doing. We have got a User struct that holds two string references, name and profile. Both of them are tied to the same lifetime 'a, meaning they're expected to stay valid as long as 'a is valid, and create_user with passing name reference, which is fine, but then we create a new string called local_profile and inside the function. Next, we take a reference to that string and try to store it in a User struct. And that's a problem. Local_profile is a local variable. It will be dropped as soon as the function ends. So returning a reference to it is unsafe. As we can see, Rust steps in here with a compile time error to stop us from returning a reference to something that's about to disappear. So if Rust allowed this, we would be printing invalid memory and potentially leading to undefined behavior. But Rust catches this and, you know, at compile time in order to prevent the bug before the program even runs. All right, so we saw the problem. Our function was returning a reference to a local_profile, which didn't live long enough. Rust stopped us from making a critical mistake by catching this as compile time. But how do we fix it? So now we know the problem, we know like what happened, how do we fix now? Key over here is ownership. Instead of borrowing profile, let's make User own the string. In this way, we don't have to worry about lifetime or references going out of scope. Let's apply this change and see how the updated code looks. All right, now as we see in the code profile is a string instead of borrowed string. This means our User struct takes full ownership of the profile data, preventing any dangling references. With this change, our program runs safely and smoothly. Let's test it. And there you have it. By using ownership instead of borrowing, we have eliminated the lifetime issue while keeping our code simple and efficient. Now that we have seen how ownership keeps our code safe, let's switch gears and talk about runtime errors, those bugs that show up while the program's still running. Even with all of Rust safety rules, things can still go wrong, like accessing an invalid index. When that happens, Rust gives a helpful stack trace for us to debug. Let's check out a quick example where we hit an out-of-bound index and see how Rust points us right to the issue. All right, here's a code snippet. We have got a vector with three elements, one, two, and three. But now we are trying to access the vector of five and let's try to run it. Boom. Panic. Why? What happened? Because vectors in Rust are zero index. So the valid indices over here are zero, one, and two. Index five, no, that's out of bound and doesn't let that slide. So instead of crashing unpredictably, Rust safely stops the program and shows us exactly where the things went wrong. If you can see in the console, this is a detailed stack trace. Rust pinpoints that exactly like, "Hey, this index is out of the bounds. The length is three, but the index is five." But here's the question, what if we don't want the program to crash? What if you want to handle this error gracefully? Well, Rust gives us a safer option, using .get. So in a new code snippet, we are going to apply this fix. We are going to use .get instead of direct indexing. Let's apply the fix. All right, with .get, Rust won't panic when we access an invalid index. Instead, it returns an option. So this option, it allows us to check if the value exists before even using it. Let's see if this works. Now as you see in the console, instead of crashing, our program gracefully handles the error. And if an index exists, we print its value. Let's see, two. Yes, it printed three. Now let's say five. "Index out of bounds." So if an index exists, print its value. Otherwise, instead of panicking, we get a friendly message. So this is one of Rust best practices using safe methods like get, you can avoid runtime crashes. And that's a wrap on common debugging challenges. We looked at ownership, we looked at lifetimes and stack traces. Each one, key to writing safe and reliable code. Rust rules might feel strict at first, but they're here to protect you, and once you get the hang of it, they really pay off. Keep practicing, keep building. And up next, we'll see how AI can make debugging in Rust even smoother. Stay tuned.

Contents