MoreRSS

site iconCorrcodeModify

This is an ongoing series of articles about idiomatic Rust and best practices.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of Corrcode

Flattening Rust's Learning Curve

2025-05-05 08:00:00

I see people make the same mistakes over and over again when learning Rust. Here are my thoughts (ordered by importance) on how you can ease the learning process. My goal is to help you save time and frustration.

Let Your Guard Down

Stop resisting. That’s the most important lesson.

Accept that learning Rust requires adopting a completely different mental model than what you’re used to. There are a ton of new concepts to learn like lifetimes, ownership, and the trait system. And depending on your background, you’ll need to add generics, pattern matching, or macros to the list.

Your learning pace doesn’t have much to do with whether you’re smart or not or if you have a lot of programming experience. Instead, what matters more is your attitude toward the language.

I have seen junior devs excel at Rust with no prior training and senior engineers struggle for weeks/months or even give up entirely. Leave your hubris at home.

Treat the borrow checker as a co-author, not an adversary. This reframes the relationship. Let the compiler do the teaching: for example, this works great with lifetimes, because the compiler will tell you when a lifetime is ambiguous. Then just add it but take the time to reason about why the compiler couldn’t figure it out itself.

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

If you try to compile this, the compiler will ask you to add a lifetime parameter. It provides this helpful suggestion:

1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

So you don’t have to guess what the compiler wants and can follow its instructions. But also sit down and wonder why the compiler couldn’t figure it out itself.

Most of the time when fighting the compiler it is actually exposing a design flaw. Similarly, if your code gets overly verbose or looks ugly, there’s probably a better way. Declare defeat and learn to do it the Rust way.

If you come from a dynamic language like Python, you’ll find that Rust is more verbose in general. Most of it just comes from type annotations, though. Some people might dismiss Rust as being “unelegant” or “ugly”, but the verbosity actually serves a good purpose and is immensely helpful for building large-scale applications:

  • First off, you will read the code more often than you write it, which means type annotations will give you more local context to reason with.
  • Second, it helps immensely with refactoring because the compiler can check if you broke any code while you move things around. If your code turns out to look very ugly, take a step back and ask if there’s a simpler solution. Don’t dismiss the language right away.

Turn on all clippy lints on day one – even the pedantic ones. Run the linter and follow the suggestions religiously. Don’t skip that step once your program compiles.

Resistance is futile. The longer you refuse to learn, the longer you will suffer; but the moment you let your guard down is the moment you’ll start to learn. Forget what you think you knew about programming and really start to listen to what the compiler, the standard library, and clippy are trying to tell you.

Baby Steps

I certainly tried to run before I could walk. That alone cost me a lot of precious time.

Don’t make it too hard on yourself in the beginning. Here are some tips:

  • Use String and clone() and unwrap generously; you can always refactor later – and refactoring is the best part about Rust! I wrote an article on saving yourself time during that phase here.
  • Use simple if or match statements before starting to learn some of the more idiomatic .and_then etc. combinators
  • Avoid async Rust in week 1. The additional rules are a tax on people still learning the core ownership model.

Don’t introduce too many new concepts at the same time! Instead, while you learn about a new concept, have an editor open and write out a few examples. What helped was to just write some code in the Rust playground and try to get it to compile. Write super small snippets (e.g., one main.rs for one concept) instead of using one big “tutorial” repo. Get into the habit of throwing most of your code away.

I still do that and test out ideas in the playground or when I brainstorm with clients.

For instance, here’s one of my favorite code snippets to explain the concept of ownership:

fn my_func(v: String) {
    // do something with v
}

fn main() {
    let s = String::from("hello");
    my_func(s);
    my_func(s); // error, but why?
}

Can you fix it? Can you explain it? Ask yourself what would change if v was an i32.

If Rust code looks scary to you, break it down. Write your own, simpler version, then slowly increase the complexity. Rust is easier to write than to read. By writing lots of Rust, you will also learn how to read it better as well.

Be Accurate

How you do anything is how you do everything. – An ancient Rust proverb

You can be sloppy in other languages, but not in Rust. That means you have to be accurate while you code or the code just won’t compile. The expectation is that this approach will save you debugging time in the future.

I found that the people who learn Rust the fastest all have great attention to detail. If you try to just get things done and move on, you will have a harder time than if you aim to do things right on your first try. You will have a much better time if you re-read your code to fix stupid typos before pressing “compile.” Also build a habit of automatically adding & and mut where necessary as you go.

A good example of someone who thinks about these details while coding is Tsoding. For example, watch this stream where he builds a search engine in Rust from scratch to see what I mean. I think you can learn this skill as long as you’re putting in your best effort and give it some time.

Don’t Cheat

With today’s tooling it is very easy to offload the bulk of the work to the computer. Initially, it will feel like you’re making quick progress, but in reality, you just strengthen bad habits in your workflow. If you can’t explain what you wrote to someone else or if you don’t know about the tradeoffs/assumptions a part of your code makes, you took it too far.

Often, this approach stems from a fear that you’re not making progress fast enough. But you don’t have to prove to someone else that you’re clever enough to pick up Rust very quickly.

Walk the Walk

To properly learn Rust you actually have to write a lot of code by hand. Don’t be a lurker on Reddit, reading through other people’s success stories. Have some skin in the game! Put in the hours because there is no silver bullet. Once it works, consider open sourcing your code even if you know it’s not perfect.

Don’t Go on Auto-Pilot

LLMs are like driving a car on auto-pilot. It’s comfortable at first, but you won’t feel in control and slowly, that uneasy feeling will creep in. Turn off the autopilot while learning.

A quick way to set you up for success is to learn by writing code in the Rust Playground first. Don’t use LLMs or code completion. Just type it out! If you can’t, that means you haven’t fully internalized a concept yet. That’s fine! Go to the standard library and read the docs. Take however long it takes and then come back and try again.

Slow is steady and steady is fast.

Build Muscle Memory

Muscle memory in programming is highly underrated. People will tell you that this is what code completion is for, but I believe it’s a requirement to reach a state of flow: if you constantly blunder over syntax errors or, worse, just wait for the next auto-completion to make progress, that is a terrible developer experience.

When writing manually, you will make more mistakes. Embrace them! These mistakes will help you learn to understand the compiler output. You will get a “feeling” for how the output looks in different error scenarios. Don’t gloss over these errors. Over time you will develop an intuition about what feels “rustic.”

Predict The Output

Another thing I like to do is to run “prediction exercises” where I guess if code will compile before running it. This builds intuition. Try to make every program free of syntax errors before you run it. Don’t be sloppy. Of course, you won’t always succeed, but you will get much better at it over time.

Try To Solve Problems Yourself, Only Then Look Up The Solution.

Read lots of other people’s code. I recommend ripgrep, for example, which is some of the best Rust code out there.

Develop A Healthy Share Of Reading/Writing Code.

Don’t be afraid to get your hands dirty. Which areas of Rust do you avoid? What do you run away from? Focus on that. Tackle your blind spots. Track your common “escape hatches” (unsafe, clone, etc.) to identify your current weaknesses. For example, if you are scared of proc macros, write a bunch of them.

Break Your Code

After you’re done with an exercise, break it! See what the compiler says. See if you can explain what happens.

Don’t Use Other People’s Crates While Learning

A poor personal version is better than a perfect external crate (at least while learning). Write some small library code yourself as an exercise. Notable exceptions are probably serde and anyhow, which can save you time dealing with JSON inputs and setting up error handling that you can spend on other tasks as long as you know how they work.

Build Good Intuitions

Concepts like lifetimes are hard to grasp. Sometimes it helps to draw how data moves through your system. Develop a habit to explain concepts to yourself and others through drawing. I’m not sure, but I think this works best for “visual”/creative people (in comparison to highly analytical people).

I personally use excalidraw for drawing. It has a “comicy” feel, which takes the edge off a bit. The implication is that it doesn’t feel highly accurate, but rather serves as a rough sketch. Many good engineers (as well as great Mathematicians and Physicists) are able to visualize concepts with sketches.

In Rust, sketches can help to visualize lifetimes and ownership of data or for architecture diagrams.

Build On Top Of What You Already Know

Earlier I said you should forget everything you know about programming. How can I claim now that you should build on top of what you already know?

What I meant is that Rust is the most different in familiar areas like control flow handling and value passing. E.g., mutability is very explicit in Rust and calling a function typically “moves” its arguments. That’s where you have to accept that Rust is just different and learn from first principles.

However, it is okay to map Rust concepts to other languages you already know. For instance, “a trait is a bit like an interface” is wrong, but it is a good starting point to understand the concept.

Here are a few more examples:

  • “A struct is like a class (minus the inheritance)”
  • “A closure is like a lambda function (but it can capture variables)”
  • “A module is like a namespace (but more powerful)”
  • “A borrow is like a pointer (but with single owner).”

And if you have a functional background, it might be:

  • Option is like the Maybe monad”
  • “Traits are like type-classes”
  • “Enums are algebraic data types”

The idea is that mapping concepts helps fill in the gaps more quickly.

Map what you already know from another language (e.g., Python, TypeScript) to Rust concepts. As long as you know that there are subtle differences, I think it’s helpful.

I don’t see people mention this a lot, but I believe that Rosetta Code is a great resource for that. You basically browse their list of tasks, pick one you like and start comparing the Rust solution with the language you’re strongest in.

Also, port code from a language you know to Rust. This way, you don’t have to learn a new domain at the same time as you learn Rust. You can build on your existing knowledge and experience.

  • Translate common language idioms from your strongest language to Rust. E.g., how would you convert a list comprehension from Python to Rust? Try it first, then look for resources, which explain the concept in Rust. For instance, I wrote one on this topic specifically.
  • I know people who have a few standard exercises that they port to every new language they learn. For example, that could be a ray-tracer, a sorting algorithm, or a small web app.

Finally, find other people who come from the same background as you. Read their blogs where they talk about their experiences learning Rust. Write down your experiences as well.

Don’t Guess

I find that people who tend to guess their way through challenges often have the hardest time learning Rust.

In Rust, the details are everything. Don’t gloss over details, because they always reveal some wisdom about the task at hand. Even if you don’t care about the details, they will come back to bite you later.

For instance, why do you have to call to_string() on a thing that’s already a string?

my_func("hello".to_string())

Those stumbling blocks are learning opportunities. It might look like a waste of time to ask these questions and means that it will take longer to finish a task, but it will pay off in the long run.

Reeeeeally read the error messages the compiler prints. Everyone thinks they do this, but time and again I see people look confused while the solution is right there in their terminal. There are hints as well; don’t ignore those. This alone will save you sooo much time. Thank me later.

You might say that is true for every language, and you’d be right. But in Rust, the error messages are actually worth your time. Some of them are like small meditations: opportunities to think about the problem at a deeper level.

If you get any borrow-checker errors, refuse the urge to guess what’s going on. Instead of guessing, walk through the data flow by hand (who owns what and when). Try to think it through for yourself and only try to compile again once you understand the problem.

Lean on Type-Driven Development

The key to good Rust code is through its type system.

It’s all in the type system. Everything you need is hidden in plain sight. But often, people skip too much of the documentation and just look at the examples.

What few people do is read the actual function documentation. You can even click through the standard library all the way to the source code to read the thing they are using. There is no magic (and that’s what’s so magical about it).

You can do that in Rust much better than in most other languages. That’s because Python for example is written in C, which requires you to cross that language boundary to learn what’s going on. Similarly, the C++ standard library isn’t a single, standardized implementation, but rather has several different implementations maintained by different organizations. That makes it super hard to know what exactly is going on. In Rust, the source code is available right inside the documentation. Make good use of that!

Function signatures tell a lot! The sooner you will embrace this additional information, the quicker you will be off to the races with Rust. If you have the time, read interesting parts of the standard library docs. Even after years, I always learn something when I do.

Try to model your own projects with types first. This is when you start to have way more fun with the language. It feels like you have a conversation with the compiler about the problem you’re trying to solve.

For example, once you learn how concepts like expressions, iterators and traits fit together, you can write more concise, readable code.

Once you learn how to encode invariants in types, you can write more correct code that you don’t have to run to test. Instead, you can’t compile incorrect code in the first place.

Learn Rust through “type-driven development” and let the compiler errors guide your design.

Invest Time In Finding Good Learning Resources

Before you start, shop around for resources that fit your personal learning style. To be honest, there is not that much good stuff out there yet. On the plus side, it doesn’t take too long to go through the list of resources before settling on one specific platform/book/course. The right resource depends on what learner you are. In the long run, finding the right resource saves you time because you will learn quicker.

I personally don’t like doing toy exercises that others have built out for me. That’s why I don’t like Rustlings too much; the exercises are not “fun” and too theoretical. I want more practical exercises. I found that Project Euler or Advent of Code work way better for me. The question comes up quite often, so I wrote a blog post about my favorite Rust learning resources.

Don’t Just Watch YouTube

I like to watch YouTube, but exclusively for recreational purposes. In my opinion, watching ThePrimeagen is for entertainment only. He’s an amazing programmer, but trying to learn how to program by watching someone else do it is like trying to learn how to become a great athlete by watching the Olympics. Similarly, I think we all can agree that Jon Gjengset is an exceptional programmer and teacher, but watching him might be overwhelming if you’re just starting out. (Love the content though!)

Same goes for conference talks or podcasts: they are great for context, and for soft-skills, but not for learning Rust.

Instead, invest in a good book if you can. Books are not yet outdated and you can read them offline, add personal notes, type out the code yourself and get a “spatial overview” of the depth of the content by flipping through the pages.

Similarly, if you’re serious about using Rust professionally, buy a course or get your boss to invest in a trainer. Of course, I’m super biased here as I run a Rust consultancy, but I truly believe that it will save you and your company countless hours and will set you up for long-term success. Think about it: you will work with this codebase for years to come. Better make that experience a pleasant one. A good trainer, just like a good teacher, will not go through the Rust book with you, but watch you program Rust in the wild and give you personalized feedback about your weak spots.

Find A Coding Buddy

“Shadow” more experienced team members or friends.

Don’t be afraid to ask for a code review on Mastodon or the Rust forum and return the favor and do code reviews there yourself. Take on opportunities for pair programming.

Explain Rust Code To Non-Rust Developers

This is such a great way to see if you truly understood a concept. Don’t be afraid to say “I don’t know.” Then go and explore the answer together by going straight to the docs. It’s way more rewarding and honest.

Help out with OSS code that is abandoned. If you put in a solid effort to fix an unmaintained codebase, you will help others while learning how to work with other people’s Rust code.

Read code out loud and explain it. There’s no shame in that! It helps you “serialize” your thoughts and avoid skipping important details.

Take notes. Write your own little “Rust glossary” that maps Rust terminology to concepts in your business domain. It doesn’t have to be complete and just has to serve your needs.

Write down things you found hard and things you learned. If you find a great learning resource, share it!

Believe In The Long-Term Benefit

If you learn Rust because you want to put it on your CV, stop. Learn something else instead.

I think you have to actually like programming (and not just the idea of it) to enjoy Rust.

If you want to be successful with Rust, you have to be in it for the long run. Set realistic expectations: You won’t be a “Rust grandmaster” in a week but you can achieve a lot in a month of focused effort. There is no silver bullet, but if you avoid the most common ways to shoot yourself in the foot, you pick up the language much faster. Rust is a day 2 language. You won’t “feel” as productive as in your first week of Go or Python, but stick it out and it will pay off. Good luck and have fun!

Svix

2025-05-01 08:00:00

We don’t usually think much about Webhooks – at least I don’t. It’s just web requests after all, right? In reality, there is a lot of complexity behind routing webhook requests through the internet.

What if a webhook request gets lost? How do you know it was received in the first place? Can it be a security issue if a webhook gets handled twice? (Spoiler alert: yes)

Microsoft

2025-04-17 08:00:00

Victor Ciura is a veteran C++ developer who worked on Visual C++ and the Clang Power Tools. In this first episode of season 4, we talk to him about large-scale Rust adoption at Microsoft.

Pitfalls of Safe Rust

2025-04-01 08:00:00

When people say Rust is a “safe language”, they often mean memory safety. And while memory safety is a great start, it’s far from all it takes to build robust applications.

Memory safety is important but not sufficient for overall reliability.

In this article, I want to show you a few common gotchas in safe Rust that the compiler doesn’t detect and how to avoid them.

Why Rust Can’t Always Help

Even in safe Rust code, you still need to handle various risks and edge cases. You need to address aspects like input validation and making sure that your business logic is correct.

Here are just a few categories of bugs that Rust doesn’t protect you from:

  • Type casting mistakes (e.g. overflows)
  • Logic bugs
  • Panics because of using unwrap or expect
  • Malicious or incorrect build.rs scripts in third-party crates
  • Incorrect unsafe code in third-party libraries
  • Race conditions

Let’s look at ways to avoid some of the more common problems. The tips are roughly ordered by how likely you are to encounter them.

Table of Contents

Click here to expand the table of contents.

Protect Against Integer Overflow

Overflow errors can happen pretty easily:

// DON'T: Use unchecked arithmetic
fn calculate_total(price: u32, quantity: u32) -> u32 {
    price * quantity  // Could overflow!
}

If price and quantity are large enough, the result will overflow. Rust will panic in debug mode, but in release mode, it will silently wrap around.

To avoid this, use checked arithmetic operations:

// DO: Use checked arithmetic operations
fn calculate_total(price: u32, quantity: u32) -> Result<u32, ArithmeticError> {
    price.checked_mul(quantity)
        .ok_or(ArithmeticError::Overflow)
}

Static checks are not removed since they don’t affect the performance of generated code. So if the compiler is able to detect the problem at compile time, it will do so:

fn main() {
    let x: u8 = 2;
    let y: u8 = 128;
    let z = x * y;  // Compile-time error!
}

The error message will be:

error: this arithmetic operation will overflow
 --> src/main.rs:4:13
  |
4 |     let z = x * y;  // Compile-time error!
  |             ^^^^^ attempt to compute `2_u8 * 128_u8`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

For all other cases, use checked_add, checked_sub, checked_mul, and checked_div, which return None instead of wrapping around on underflow or overflow. 1

Quick Tip: Enable Overflow Checks In Release Mode

Rust carefully balances performance and safety. In scenarios where a performance hit is acceptable, memory safety takes precedence. 1

Integer overflows can lead to unexpected results, but they are not inherently unsafe. On top of that, overflow checks can be expensive, which is why Rust disables them in release mode. 2

However, you can re-enable them in case your application can trade the last 1% of performance for better overflow detection.

Put this into your Cargo.toml:

[profile.release]
overflow-checks = true # Enable integer overflow checks in release mode

This will enable overflow checks in release mode. As a consequence, the code will panic if an overflow occurs.

See the docs for more details.

  1. One example where Rust accepts a performance cost for safety would be checked array indexing, which prevents buffer overflows at runtime. Another is when the Rust maintainers fixed float casting because the previous implementation could cause undefined behavior when casting certain floating point values to integers.

  2. According to some benchmarks, overflow checks cost a few percent of performance on typical integer-heavy workloads. See Dan Luu’s analysis here

Avoid as For Numeric Conversions

While we’re on the topic of integer arithmetic, let’s talk about type conversions. Casting values with as is convenient but risky unless you know exactly what you are doing.

let x: i32 = 42;
let y: i8 = x as i8;  // Can overflow!

There are three main ways to convert between numeric types in Rust:

  1. ⚠️ Using the as keyword: This approach works for both lossless and lossy conversions. In cases where data loss might occur (like converting from i64 to i32), it will simply truncate the value.

  2. Using From::from(): This method only allows lossless conversions. For example, you can convert from i32 to i64 since all 32-bit integers can fit within 64 bits. However, you cannot convert from i64 to i32 using this method since it could potentially lose data.

  3. Using TryFrom: This method is similar to From::from() but returns a Result instead of panicking. This is useful when you want to handle potential data loss gracefully.

Quick Tip: Safe Numeric Conversions

If in doubt, prefer From::from() and TryFrom over as.

  • use From::from() when you can guarantee no data loss.
  • use TryFrom when you need to handle potential data loss gracefully.
  • only use as when you’re comfortable with potential truncation or know the values will fit within the target type’s range and when performance is absolutely critical.

(Adapted from StackOverflow answer by delnan and additional context.)

The as operator is not safe for narrowing conversions. It will silently truncate the value, leading to unexpected results.

What is a narrowing conversion? It’s when you convert a larger type to a smaller type, e.g. i32 to i8.

For example, see how as chops off the high bits from our value:

fn main() {
    let a: u16 = 0x1234;
    let b: u8 = a as u8;
    println!("0x{:04x}, 0x{:02x}", a, b); // 0x1234, 0x34
}

So, coming back to our first example above, instead of writing

let x: i32 = 42;
let y: i8 = x as i8;  // Can overflow!

use TryFrom instead and handle the error gracefully:

let y = i8::try_from(x).ok_or("Number is too big to be used here")?;

Use Bounded Types for Numeric Values

Bounded types make it easier to express invariants and avoid invalid states.

E.g. if you have a numeric type and 0 is never a correct value, use std::num::NonZeroUsize instead.

You can also create your own bounded types:

// DON'T: Use raw numeric types for domain values
struct Measurement {
    distance: f64,  // Could be negative!
}

// DO: Create bounded types
#[derive(Debug, Clone, Copy)]
struct Distance(f64);

impl Distance {
    pub fn new(value: f64) -> Result<Self, DistanceError> {
        if value < 0.0 || !value.is_finite() {
            return Err(DistanceError::Invalid);
        }
        Ok(Distance(value))
    }
}

struct Measurement {
    distance: Distance,
}

(Rust Playground)

Don’t Index Into Arrays Without Bounds Checking

Whenever I see the following, I get goosebumps 😨:

let arr = [1, 2, 3];
let elem = arr[3];  // Panic!

That’s a common source of bugs. Unlike C, Rust does check array bounds and prevents a security vulnerability, but it still panics at runtime.

Instead, use the get method:

let elem = arr.get(3);

It returns an Option which you can now handle gracefully.

See this blog post for more info on the topic.

Use split_at_checked Instead Of split_at

This issue is related to the previous one. Say you have a slice and you want to split it at a certain index.

let mid = 4;
let arr = [1, 2, 3];
let (left, right) = arr.split_at(mid);

You might expect that this returns a tuple of slices where the first slice contains all elements and the second slice is empty.

Instead, the above code will panic because the mid index is out of bounds!

To handle that more gracefully, use split_at_checked instead:

let arr = [1, 2, 3];
// This returns an Option
match arr.split_at_checked(mid) {
    Some((left, right)) => {
        // Do something with left and right
    }
    None => {
        // Handle the error
    }
}

This returns an Option which allows you to handle the error case. (Rust Playground)

More info about split_at_checked here.

Avoid Primitive Types For Business Logic

It’s very tempting to use primitive types for everything. Especially Rust beginners fall into this trap.

// DON'T: Use primitive types for usernames
fn authenticate_user(username: String) {
    // Raw String could be anything - empty, too long, or contain invalid characters
}

However, do you really accept any string as a valid username? What if it’s empty? What if it contains emojis or special characters?

You can create a custom type for your domain instead:

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Username(String);

impl Username {
    pub fn new(name: &str) -> Result<Self, UsernameError> {
        // Check for empty username
        if name.is_empty() {
            return Err(UsernameError::Empty);
        }

        // Check length (for example, max 30 characters)
        if name.len() > 30 {
            return Err(UsernameError::TooLong);
        }

        // Only allow alphanumeric characters and underscores
        if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
            return Err(UsernameError::InvalidCharacters);
        }

        Ok(Username(name.to_string()))
    }

    /// Allow to get a reference to the inner string
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

fn authenticate_user(username: Username) {
    // We know this is always a valid username!
    // No empty strings, no emojis, no spaces, etc.
}

(Rust playground)

Make Invalid States Unrepresentable

The next point is closely related to the previous one.

Can you spot the bug in the following code?

// DON'T: Allow invalid combinations
struct Configuration {
    port: u16,
    host: String,
    ssl: bool,
    ssl_cert: Option<String>, 
}

The problem is that you can have ssl set to true but ssl_cert set to None. That’s an invalid state! If you try to use the SSL connection, you can’t because there’s no certificate. This issue can be detected at compile-time:

Use types to enforce valid states:

// First, let's define the possible states for the connection
enum ConnectionSecurity {
    Insecure,
    // We can't have an SSL connection
    // without a certificate!
    Ssl { cert_path: String },
}

struct Configuration {
    port: u16,
    host: String,
    // Now we can't have an invalid state!
    // Either we have an SSL connection with a certificate
    // or we don't have SSL at all.
    security: ConnectionSecurity,
}

In comparison to the previous section, the bug was caused by an invalid combination of closely related fields. To prevent that, clearly map out all possible states and transitions between them. A simple way is to define an enum with optional metadata for each state.

If you’re curious to learn more, here is a more in-depth blog post on the topic.

Handle Default Values Carefully

It’s quite common to add a blanket Default implementation to your types. But that can lead to unforeseen issues.

For example, here’s a case where the port is set to 0 by default, which is not a valid port number.2

// DON'T: Implement `Default` without consideration
#[derive(Default)]  // Might create invalid states!
struct ServerConfig {
    port: u16,      // Will be 0, which isn't a valid port!
    max_connections: usize,
    timeout_seconds: u64,
}

Instead, consider if a default value makes sense for your type.

// DO: Make Default meaningful or don't implement it
struct ServerConfig {
    port: Port,
    max_connections: NonZeroUsize,
    timeout_seconds: Duration,
}

impl ServerConfig {
    pub fn new(port: Port) -> Self {
        Self {
            port,
            max_connections: NonZeroUsize::new(100).unwrap(),
            timeout_seconds: Duration::from_secs(30),
        }
    }
}

Implement Debug Safely

If you blindly derive Debug for your types, you might expose sensitive data. Instead, implement Debug manually for types that contain sensitive information.

// DON'T: Expose sensitive data in debug output
#[derive(Debug)]
struct User {
    username: String,
    password: String,  // Will be printed in debug output!
}

Instead, you could write:

// DO: Implement Debug manually
#[derive(Debug)]
struct User {
    username: String,
    password: Password,
}

struct Password(String);

impl std::fmt::Debug for Password {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("[REDACTED]")
    }
}

fn main() {
    let user = User {
        username: String::from(""),
        password: Password(String::from("")),
    };
    println!("{user:#?}");
}

This prints

User {
    username: "",
    password: [REDACTED],
}

(Rust playground)

For production code, use a crate like secrecy.

However, it’s not black and white either: If you implement Debug manually, you might forget to update the implementation when your struct changes. A common pattern is to destructure the struct in the Debug implementation to catch such errors.

Instead of this:

// don't
impl std::fmt::Debug for DatabaseURI {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}://{}:[REDACTED]@{}/{}", self.scheme, self.user, self.host, self.database)
    }
}

How about destructuring the struct to catch changes?

// do
impl std::fmt::Debug for DatabaseURI {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
       // Destructure the struct to catch changes
       // This way, the compiler will warn you if you add a new field
       // and forget to update the Debug implementation
        let DatabaseURI { scheme, user, password: _, host, database, } = self;
        write!(f, "{scheme}://{user}:[REDACTED]@{host}/{database}")?;
        // -- or --
        // f.debug_struct("DatabaseURI")
        //     .field("scheme", scheme)
        //     .field("user", user)
        //     .field("password", &"***")
        //     .field("host", host)
        //     .field("database", database)
        //     .finish()

        Ok(())
    }
}

(Rust playground)

Thanks to Wesley Moore (wezm) for the hint and to Simon Brüggen (m3t0r) for the example.

Careful With Serialization

Don’t blindly derive Serialize and Deserialize – especially for sensitive data. The values you read/write might not be what you expect!

// DON'T: Blindly derive Serialize and Deserialize 
#[derive(Serialize, Deserialize)]
struct UserCredentials {
    #[serde(default)]  // ⚠️ Accepts empty strings when deserializing!
    username: String,
    #[serde(default)]
    password: String, // ⚠️ Leaks the password when serialized!
}

When deserializing, the fields might be empty. Empty credentials could potentially pass validation checks if not properly handled

On top of that, the serialization behavior could also leak sensitive data. By default, Serialize will include the password field in the serialized output, which could expose sensitive credentials in logs, API responses, or debug output.

A common fix is to implement your own custom serialization and deserialization methods by using impl<'de> Deserialize<'de> for UserCredentials.

The advantage is that you have full control over input validation. However, the disadvantage is that you need to implement all the logic yourself.

An alternative strategy is to use the #[serde(try_from = "FromType")] attribute.

Let’s take the Password field as an example. Start by using the newtype pattern to wrap the standard types and add custom validation:

#[derive(Deserialize)]
// Tell serde to call `Password::try_from` with a `String`
#[serde(try_from = "String")]
pub struct Password(String);

Now implement TryFrom for Password:

impl TryFrom<String> for Password {
    type Error = PasswordError;

    /// Create a new password
    ///
    /// Throws an error if the password is too short.
    /// You can add more checks here.
    fn try_from(value: String) -> Result<Self, Self::Error> {
        // Validate the password
        if value.len() < 8 {
            return Err(PasswordError::TooShort);
        }
        Ok(Password(value))
    }
}

With this trick, you can no longer deserialize invalid passwords:

// Panic: password too short!
let password: Password = serde_json::from_str(r#""pass""#).unwrap();

(Try it on the Rust Playground)

Credits go to EqualMa’s article on dev.to and to Alex Burka (durka) for the hint.

Protect Against Time-of-Check to Time-of-Use (TOCTOU)

This is a more advanced topic, but it’s important to be aware of it. TOCTOU (time-of-check to time-of-use) is a class of software bugs caused by changes that happen between when you check a condition and when you use a resource.

// DON'T: Vulnerable approach with separate check and use
fn remove_dir(path: &Path) -> io::Result<()> {
    // First check if it's a directory
    if !path.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::NotADirectory,
            "not a directory"
        ));
    }
    
    // TOCTOU vulnerability: Between the check above and the use below,
    // the path could be replaced with a symlink to a directory we shouldn't access!
    remove_dir_impl(path)
}

(Rust playground)

The safer approach opens the directory first, ensuring we operate on what we checked:

// DO: Safer approach that opens first, then checks
fn remove_dir(path: &Path) -> io::Result<()> {
    // Open the directory WITHOUT following symlinks
    let handle = OpenOptions::new()
        .read(true)
        .custom_flags(O_NOFOLLOW | O_DIRECTORY) // Fails if not a directory or is a symlink
        .open(path)?;
    
    // Now we can safely remove the directory contents using the open handle
    remove_dir_impl(&handle)
}

(Rust playground)

Here’s why it’s safer: while we hold the handle, the directory can’t be replaced with a symlink. This way, the directory we’re working with is the same as the one we checked. Any attempt to replace it won’t affect us because the handle is already open.

You’d be forgiven if you overlooked this issue before. In fact, even the Rust core team missed it in the standard library. What you saw is a simplified version of an actual bug in the std::fs::remove_dir_all function. Read more about it in this blog post about CVE-2022-21658.

Use Constant-Time Comparison for Sensitive Data

Timing attacks are a nifty way to extract information from your application. The idea is that the time it takes to compare two values can leak information about them. For example, the time it takes to compare two strings can reveal how many characters are correct. Therefore, for production code, be careful with regular equality checks when handling sensitive data like passwords.

// DON'T: Use regular equality for sensitive comparisons
fn verify_password(stored: &[u8], provided: &[u8]) -> bool {
    stored == provided  // Vulnerable to timing attacks!
}

// DO: Use constant-time comparison
use subtle::{ConstantTimeEq, Choice};

fn verify_password(stored: &[u8], provided: &[u8]) -> bool {
    stored.ct_eq(provided).unwrap_u8() == 1
}

Don’t Accept Unbounded Input

Protect Against Denial-of-Service Attacks with Resource Limits. These happen when you accept unbounded input, e.g. a huge request body which might not fit into memory.

// DON'T: Accept unbounded input
fn process_request(data: &[u8]) -> Result<(), Error> {
    let decoded = decode_data(data)?;  // Could be enormous!
    // Process decoded data
    Ok(())
}

Instead, set explicit limits for your accepted payloads:

const MAX_REQUEST_SIZE: usize = 1024 * 1024;  // 1MiB

fn process_request(data: &[u8]) -> Result<(), Error> {
    if data.len() > MAX_REQUEST_SIZE {
        return Err(Error::RequestTooLarge);
    }
    
    let decoded = decode_data(data)?;
    // Process decoded data
    Ok(())
}

Surprising Behavior of Path::join With Absolute Paths

If you use Path::join to join a relative path with an absolute path, it will silently replace the relative path with the absolute path.

use std::path::Path;

fn main() {
    let path = Path::new("/usr").join("/local/bin");
    println!("{path:?}"); // Prints "/local/bin" 
}

This is because Path::join will return the second path if it is absolute.

I was not the only one who was confused by this behavior. Here’s a thread on the topic, which also includes an answer by Johannes Dahlström:

The behavior is useful because a caller […] can choose whether it wants to use a relative or absolute path, and the callee can then simply absolutize it by adding its own prefix and the absolute path is unaffected which is probably what the caller wanted. The callee doesn’t have to separately check whether the path is absolute or not.

And yet, I still think it’s a footgun. It’s easy to overlook this behavior when you use user-provided paths. Perhaps join should return a Result instead? In any case, be aware of this behavior.

Check For Unsafe Code In Your Dependencies With cargo-geiger

So far, we’ve only covered issues with your own code. For production code, you also need to check your dependencies. Especially unsafe code would be a concern. This can be quite challenging, especially if you have a lot of dependencies.

cargo-geiger is a neat tool that checks your dependencies for unsafe code. It can help you identify potential security risks in your project.

cargo install cargo-geiger
cargo geiger

This will give you a report of how many unsafe functions are in your dependencies. Based on this, you can decide if you want to keep a dependency or not.

Clippy Can Prevent Many Of These Issues

Here is a set of clippy lints that can help you catch these issues at compile time. See for yourself in the Rust playground.

Here’s the gist:

  • cargo check will not report any issues.
  • cargo run will panic or silently fail at runtime.
  • cargo clippy will catch all issues at compile time (!) 😎
// Arithmetic
#![deny(arithmetic_overflow)] // Prevent operations that would cause integer overflow
#![deny(clippy::checked_conversions)] // Suggest using checked conversions between numeric types
#![deny(clippy::cast_possible_truncation)] // Detect when casting might truncate a value
#![deny(clippy::cast_sign_loss)] // Detect when casting might lose sign information
#![deny(clippy::cast_possible_wrap)] // Detect when casting might cause value to wrap around
#![deny(clippy::cast_precision_loss)] // Detect when casting might lose precision
#![deny(clippy::integer_division)] // Highlight potential bugs from integer division truncation
#![deny(clippy::arithmetic_side_effects)] // Detect arithmetic operations with potential side effects
#![deny(clippy::unchecked_duration_subtraction)] // Ensure duration subtraction won't cause underflow

// Unwraps
#![warn(clippy::unwrap_used)] // Discourage using .unwrap() which can cause panics
#![warn(clippy::expect_used)] // Discourage using .expect() which can cause panics
#![deny(clippy::panicking_unwrap)] // Prevent unwrap on values known to cause panics
#![deny(clippy::option_env_unwrap)] // Prevent unwrapping environment variables which might be absent

// Array indexing
#![deny(clippy::indexing_slicing)] // Avoid direct array indexing and use safer methods like .get()

// Path handling
#![deny(clippy::join_absolute_paths)] // Prevent issues when joining paths with absolute paths

// Serialization issues
#![deny(clippy::serde_api_misuse)] // Prevent incorrect usage of Serde's serialization/deserialization API

// Unbounded input
#![deny(clippy::uninit_vec)] // Prevent creating uninitialized vectors which is unsafe

// Unsafe code detection
#![deny(clippy::transmute_int_to_char)] // Prevent unsafe transmutation from integers to characters
#![deny(clippy::transmute_int_to_float)] // Prevent unsafe transmutation from integers to floats
#![deny(clippy::transmute_ptr_to_ref)] // Prevent unsafe transmutation from pointers to references
#![deny(clippy::transmute_undefined_repr)] // Detect transmutes with potentially undefined representations

use std::path::Path;
use std::time::Duration;

fn main() {
    // ARITHMETIC ISSUES

    // Integer overflow: This would panic in debug mode and silently wrap in release
    let a: u8 = 255;
    let _b = a + 1;

    // Unsafe casting: Could truncate the value
    let large_number: i64 = 1_000_000_000_000;
    let _small_number: i32 = large_number as i32;

    // Sign loss when casting
    let negative: i32 = -5;
    let _unsigned: u32 = negative as u32;

    // Integer division can truncate results
    let _result = 5 / 2; // Results in 2, not 2.5

    // Duration subtraction can underflow
    let short = Duration::from_secs(1);
    let long = Duration::from_secs(2);
    let _negative = short - long; // This would underflow

    // UNWRAP ISSUES

    // Using unwrap on Option that could be None
    let data: Option<i32> = None;
    let _value = data.unwrap();

    // Using expect on Result that could be Err
    let result: Result<i32, &str> = Err("error occurred");
    let _value = result.expect("This will panic");

    // Trying to get environment variable that might not exist
    let _api_key = std::env::var("API_KEY").unwrap();

    // ARRAY INDEXING ISSUES

    // Direct indexing without bounds checking
    let numbers = vec![1, 2, 3];
    let _fourth = numbers[3]; // This would panic

    // Safe alternative with .get()
    if let Some(fourth) = numbers.get(3) {
        println!("{fourth}");
    }

    // PATH HANDLING ISSUES

    // Joining with absolute path discards the base path
    let base = Path::new("/home/user");
    let _full_path = base.join("/etc/config"); // Results in "/etc/config", base is ignored

    // Safe alternative
    let base = Path::new("/home/user");
    let relative = Path::new("config");
    let full_path = base.join(relative);
    println!("Safe path joining: {:?}", full_path);

    // UNSAFE CODE ISSUES

    // Creating uninitialized vectors (could cause undefined behavior)
    let mut vec: Vec<String> = Vec::with_capacity(10);
    unsafe {
        vec.set_len(10); // This is UB as Strings aren't initialized
    }
}

Conclusion

Phew, that was a lot of pitfalls! How many of them did you know about?

Even if Rust is a great language for writing safe, reliable code, developers still need to be disciplined to avoid bugs.

A lot of the common mistakes we saw have to do with Rust being a systems programming language: In computing systems, a lot of operations are performance critical and inherently unsafe. We are dealing with external systems outside of our control, such as the operating system, hardware, or the network. The goal is to build safe abstractions on top of an unsafe world.

Rust shares an FFI interface with C, which means that it can do anything C can do. So, while some operations that Rust allows are theoretically possible, they might lead to unexpected results.

But not all is lost! If you are aware of these pitfalls, you can avoid them, and with the above clippy lints, you can catch most of them at compile time.

That’s why testing, linting, and fuzzing are still important in Rust.

For maximum robustness, combine Rust’s safety guarantees with strict checks and strong verification methods.

Let an Expert Review Your Rust Code

I hope you found this article helpful! If you want to take your Rust code to the next level, consider a code review by an expert. I offer code reviews for Rust projects of all sizes. Get in touch to learn more.

  1. There’s also methods for wrapping and saturating arithmetic, which might be useful in some cases. It’s worth it to check out the std::intrinsics documentation to learn more.

  2. Port 0 usually means that the OS will assign a random port for you. So, TcpListener::bind("127.0.0.1:0").unwrap() is valid, but it might not be supported on all operating systems or it might not be what you expect. See the TcpListener::bind docs for more info.

Rust Learning Resources 2025

2025-03-06 08:00:00

Want to finally learn Rust?

When I ask developers what they look for in a Rust learning resource, I tend to get the same answers:

  • They want hands-on learning experiences
  • They need content that’s applicable to their work
  • They look for up-to-date material from experienced developers
  • They prefer to use their own development environment

All of the above are valid points, especially for learning Rust – a language known for its notoriously steep learning curve.

If you’ve been thinking about learning Rust for a while now and perhaps you’ve already started dabbling with it, now’s the time to fully commit. I’ve put together my favorite Rust learning resources for 2025 to help you jumpstart your Rust journey. I made sure to include a healthy mix of self-paced resources, interactive exercises, and hands-on workshops.

Rustlings

Rustlings on the terminal

Key Points:

  • Level: Beginner to Intermediate
  • Focus: Small exercises / Rust basics
  • Time: A few minutes to a few hours

The classic Rust learning resource. If it was a cocktail, it would be an Old Fashioned. Rustlings works great for beginners and for anyone wanting a quick refresher on specific Rust concepts.

You can run Rustlings from your command line, and it guides you through a series of exercises. All it takes is running a few commands in your terminal:

cargo install rustlings
rustlings init
cd rustlings/
rustlings

Go to the official Rustlings repository to learn more.

Rustfinity

Rustfinity Homepage

Key Points:

  • Level: Beginner to Intermediate
  • Focus: Small exercises / Rust basics
  • Time: A few minutes to a few hours

Rustfinity is a bit like Rustlings, but more modern and structured. It gives you a browser interface that guides you through each exercise and runs tests to check your solutions. No need to set up anything locally, which makes it great for workshops or learning on the go.

You start with “Hello, World!” and work your way up to more complex exercises. It’s a relatively new resource, but I’ve tried several exercises myself and enjoyed the experience.

They also host “Advent of Rust” events with some more challenging problems available here.

Learn more at the Rustfinity website.

100 Exercises To Learn Rust

100 Exercises To Learn Rust

Key Points:

  • Level: Beginner to Intermediate
  • Focus: Small exercises / Rust basics
  • Time: A few minutes to a few hours

This is another relatively new resource by Luca Palmieri, who is the author of the popular “Zero To Production” book. It’s a collection of 100 exercises that help you learn Rust. The course is based on the “learning by doing” principle and designed to be interactive and hands-on.

You can work through the material in your browser, or download a PDF or buy a printed copy for offline reading. The course comes with a local CLI tool called wr that verifies your solutions.

You can learn more about the course here.

CodeCrafters

CodeCrafters Rust track

Key Points:

  • Level: Intermediate to Advanced
  • Focus: Real-world projects / Systems programming
  • Time: A few days to a few weeks per project (depending on your experience)

If you already know how to code some Rust and want to take it one step further, CodeCrafters is currently the best resource for learning advanced Rust concepts in your own time. I like that they focus on real-world systems programming projects, which is where Rust is really strong.

You’ll learn how to build your own shell, HTTP server, Redis, Kafka, Git, SQLite, or DNS server from scratch.

Most people work on the projects on evenings and weekends, and it takes a few days or weeks to complete a project, but the sense of accomplishment when you finish is incredible: you’ll complete a project that teaches you both Rust and the inner workings of systems you use daily.

Try CodeCrafters For Free

CodeCrafters is the platform I genuinely recommend to friends after they’ve learned the basics of Rust. It’s the next best thing after a personal mentor or workshop.

You can try CodeCrafters for free here and get 40% off if you upgrade to a paid plan later. Full disclosure: I receive a commission for new subscriptions, but I would recommend CodeCrafters even if I didn’t.

On top of that, most companies will reimburse educational resources through their L&D budget, so check with your manager about getting reimbursed.

Workshops

Write Yourself a Shell Workshop

Key Points:

  • Level: Beginner to Intermediate
  • Focus: Focused exercises and small projects
  • Time: A few days of focused effort

I’m biased here, but nothing beats a hands-on workshop with a Rust expert. Learning from experienced Rust developers is probably the quickest way to get up to speed with Rust – especially if you plan to use Rust at work. That’s because trainers can provide you with personalized feedback and guidance, and help you avoid common pitfalls. It’s like having a personal trainer for your Rust learning journey.

My workshops are designed to be hands-on and tailored to the needs of the participants. I want people to walk away with a finished project they can extend.

All course material is open source and freely available on GitHub:

You can go through the material on your own to see if it fits your needs. Once you’re ready, feel free to reach out about tailoring the content for you and your team.

Speed Up Your Learning Process

Is your company considering a switch to Rust?

Rust is known for its steep learning curve, but with the right resources and guidance, you can become proficient in a matter of weeks. I offer hands-on workshops and training for teams and individuals who want to accelerate their learning process.

Check out my services page or send me an email to learn more.

Season 3 - Finale

2025-02-06 08:00:00

You know the drill by now. It’s time for another recap!

Sit back, get a warm beverage and look back at the highlights of Season 3 with us.

We’ve been at this for a while now (three seasons, one year, and 24 episodes to be exact). We had guests from a wide range of industries: from automotive to CAD software, and from developer tooling to systems programming.

Our focus this time around was on the technical details of Rust in production, especially integration of Rust into existing codebases and ecosystem deep dives. Thanks to everyone who participated in the survey last season, which helped us dial in our content. Let us know if we hit the mark or missed it!