Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Rust Send and Sync in Simple terms

by Ugochukwu Chizaram Omumusinachi

.

Updated Fri May 16 2025

.
Rust Send and Sync in Simple terms

Introduction

Take a look at this error:


error[E0277]: `YourType` cannot be sent between threads safely
    --> src/main.rs:10:5
     |
10  |     thread::spawn(move || {
     |     ^^^^^^^^^^^^^ `YourType` cannot be sent between threads safely
     |
     = help: within `YourType`, the trait `Send` is not implemented for `SomeInnerType`
     = note: required because it appears within the type `YourType`
     = note: required because it is used within this closure
     = note: required because it is used as the type of argument 1

If you have ever come across this type of error and you are wondering what Send and Sync are, you are at the right place. In this article, we will explain what Send and Sync are in simple terms. By the end of this article, you will understand what these traits are and how to avoid those gnarly errors.

What are Send and Sync?

In simple terms, they are traits. If you do not really understand traits, you can ensure you come back to this site sometime later—we will write on that topic. For now, just know that traits are a simple way to define shared behavior, sort of like interfaces or Rust's own version of polymorphism.

However, our initial definition is a little misleading. Send and Sync are not just any traits but marker traits. Marker traits are traits that do not have any function implementations and are only there to mark the status of their implementors. They are used at compile time to ensure certain standards are met. Some other examples are Sized and Unpin (stick around, we will be talking about Sized very soon). Also, Send and Sync are auto traits, which means if all your internal types implement them, your type also implements them.

So they are auto marker traits that allow the compiler to know a couple of things during compile time:

  • Send: If a type is safe to send into a thread, basically if you can pass ownership of the type into and out of a thread.

  • Sync: If a &T of the type can be sent into a thread, basically if a shared reference of the type can be sent or shared between threads.

Primitive Types and Their Send and Sync Status

Since Send and Sync are auto traits, primitive types in Rust generally implement both traits. However, there are exceptions. Here's a breakdown:

Types That Are Both Send and Sync

  • All numeric types: i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, f32, f64

  • bool

  • char

  • (), the unit type

  • Arrays and tuples, if their elements are Send and Sync

Types That Are Only Send (but not Sync)

  • Some raw pointers can be Send depending on their usage, but they require careful handling

Types That Are Neither Send Nor Sync

  • Rc<T> (Reference Counted pointer)

  • UnsafeCell<T> (used for interior mutability)

  • Raw pointers (*const T, *mut T) by default, unless explicitly made safe

If your custom type, including std::types like Cow, collections, etc., is composed only of types that are Send and Sync, it will automatically implement both traits. Otherwise, you may need to adjust your design or use synchronization primitives like Arc<T> or Mutex<T> to ensure thread safety.

What is the Difference Between Send and Sync?

If you've asked yourself this question, do not worry—you are not alone. You just need to understand the difference between Send and Sync in Rust.

As explained earlier, a Send type can be sent into a thread, while a Sync type can be shared between threads. If you have both, then you can be sure that you can send and share the type between threads.

Let's go over this code sample. We will gradually work with this code until it works perfectly, and we hope you will understand these concepts better by the end.

First, create a project, call it whatever you want, then open the main.rs file.


fn main() {
    println!("Let's get started");
}

That is what your main function would probably look like.

We will now create a scoped thread (something we will be talking about soon, so stick around). We are using scoped threads so you are not worried about lifetime errors. Basically, they will help us share non-static Send + Sync variables in threads.

Go ahead and update your main.rs function to this:


use std::thread;

fn main() {
    let value = String::from("value");

    // create a thread scope 
    thread::scope(|s| {

        // spawn thread one 
        s.spawn(|| {
            dbg!(&value)
        });

        // spawn thread two
        s.spawn(|| {
            dbg!(&value);
        });
    });
}

Notice how we are able to reference the value in two different threads. This means this value is Sync. To check that it is Send, let us try moving it into one of the threads.

You can do this by updating your main.rs function to this:


use std::thread;

fn main() {
    let value = String::from("value");

    // create a thread scope 
    thread::scope(|s| {

        // spawn thread one 
        s.spawn(move || {
            dbg!(value)
        });

        // // spawn thread two
        // s.spawn(|| {
        //     dbg!(&value);
        // });
    });
}

Now we are not referencing it anymore but taking an owned value, since the move keyword forces variables used in a thread or closure to be owned by that scope. This compiles because this type is Send, hence we can say this type is both Send and Sync.

It is worth noting that this would work for every Send and Sync type we mentioned earlier.

  • Note! Types that implement the Copy trait would simply be copied when a move is called, hence this kind of code would still compile:


use std::thread;

fn main() {
    let value = 0;

    // create a thread scope 
    thread::scope(|s| {

        // spawn thread one 
        s.spawn(move || {
            dbg!(value)
        });

        // spawn thread two
        s.spawn(|| {
            dbg!(&value);
        });
    });
}

Now that you understand this, try tweaking that example by creating a struct and putting Send and Sync types in it to check if it is also Send and Sync.

You can share your observations.

What is Not Send and Sync and Why?

First, you should ask, why would anything not be Send or Sync? Well, these two traits revolve around thread safety. Since threads can run in parallel, actions with shared variables have to be atomic. In simple terms, if a type allows non-atomic mutable interaction, then it is not safe for Sync (sharing across threads), since two threads can mistakenly run into a race condition or cause undefined behaviors. Similarly, a type won't be Send if it contains a type that is not Send or if it implements reference counting in a non-atomic way.

Raw pointers are neither safe for sending nor syncing unless explicitly marked as such, since you have to ensure their usage is atomic.

Let's show an example of some of our earlier described types that are unsafe for Send and Sync:

Update your main code to this:


use std::{cell::RefCell, thread};

fn main() {
    let value = RefCell::new(0);

    // create a thread scope 
    thread::scope(|s| {

        // spawn thread one 
        s.spawn(|| {
            dbg!(&value)
        });

        // spawn thread two
        s.spawn(|| {
            dbg!(&value);
        });
    });
}

All we did was change the String in the initial example to a RefCell, and we get this error:


`&RefCell<{integer}>` cannot be sent between threads safely
the trait `Sync` is not implemented for `RefCell<{integer}>`
required for `&RefCell<{integer}>` to implement `Send`

This proves our point. Since RefCells allow non-atomic or basically unguarded mutability, we cannot be sure to allow two threads to share them since we could run into a race condition as both threads keep changing the variable, or even undefined behavior. To avoid this, we could use a type that allows internal mutability but in an atomic way.

We could use Mutex or RwLock here—these two allow for mutable referencing atomically.

Update the code to this:


use std::{sync::Mutex, thread};

fn main() {
    let value = Mutex::new(0);

    // create a thread scope 
    thread::scope(|s| {

        // spawn thread one 
        s.spawn(|| {
            dbg!(&value)
        });

        // spawn thread two
        s.spawn(|| {
            dbg!(&value);
        });
    });
}

Now the code runs without any errors. But what if we wanted to have owned references of the values? We would need a reference-counted style to encapsulate them, so let's use the Rc: this is a smart pointer that uses non-atomic reference counting to identify when a type has been borrowed and uses this count to identify when to drop the type.

Update code to this:


use std::{rc::Rc, sync::Mutex, thread};

fn main() {
    let value = Rc::from(Mutex::new(0));

    // create a ref counted clone of the value
    let sec_value_ref = value.clone();

    // create a thread scope and move the first clone into it
    thread::scope(move |s| {

        // spawn thread one 
        s.spawn(|| {
            dbg!(&value)
        });

        // spawn thread two
        s.spawn(|| {
            dbg!(&value);
        });
    });
}

Immediately we get this error:


`&Rc<Mutex<{integer}>>` cannot be sent between threads safely
the trait `Sync` is not implemented for `Rc<Mutex<{integer}>>`
required for `&Rc<Mutex<{integer}>>` to implement `Send`

Notice that it says Send and Sync are not implemented for this type. This is because Rc is neither Send nor Sync, which means this is not safe at all. To fix this, we would use the thread-safe version of Rc that implements reference counting by using atomic operations, called Arc:


use std::{sync::{Arc, Mutex}, thread};

fn main() {
    let value = Arc::from(Mutex::new(0));

    // create a ref counted clone of the value
    let sec_value_ref = value.clone();

    // create a thread scope and move the first clone into it
    thread::scope(move |s| {

        // spawn thread one 
        s.spawn(move || {
            dbg!(value)
        });

        // spawn thread two
        s.spawn(|| {
            dbg!(sec_value_ref);
        });
    });
}

Glossary of Terms

Here's a quick reference for important terms used throughout this article:

Atomic operations: Operations that complete in a single step relative to other threads, without being interrupted. These operations are essential for thread safety.

Auto traits: Traits that are automatically implemented for a type if all of its component types also implement the trait.

Closure: An anonymous function that can capture values from its surrounding scope. Often used in Rust for passing operations to threads.

Interior mutability: A design pattern in Rust that allows you to mutate data even when there are immutable references to that data, through the use of special types like RefCell and Mutex.

Marker traits: Traits that don't define any methods but serve to "mark" types with certain properties. Send and Sync are prime examples.

Mutex: Short for "mutual exclusion." A synchronization primitive that prevents multiple threads from accessing shared data simultaneously.

Race condition: A bug that occurs when the timing or ordering of events affects a program's correctness, usually in multi-threaded contexts.

Reference counting: A technique for memory management where objects are only deleted when no references to them exist. Rc and Arc implement this in Rust.

Scoped threads: Threads that are guaranteed to complete before the scope ends, allowing them to safely borrow data from the parent scope.

Smart pointer: A data structure that acts like a pointer but also has additional metadata and capabilities, such as Rc, Arc, Box, etc.

Thread safety: The property of code that can correctly handle multiple threads accessing it simultaneously without causing bugs.

Undefined behavior: Program behavior that is not specified by the language and could result in crashes, incorrect results, or security vulnerabilities.

Summary

  • TIP! A type is Send if it can move ownership into threads, while Sync lets you share references between threads safely.

Now you understand the clear difference between both traits. To strengthen your knowledge, try writing a simple multi-threaded counter using different types.

For more information, check out:

So that is it , you now know how to handle that error . If you want more knowledge on using rust see our course.

Course image
Become a Rust Backend Engineeer today

All-in-one Rust course for learning backend engineering with Rust. This comprehensive course is designed for Rust developers seeking proficiency in Rust.

Start Learning Now

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week