Back to Blog
    Programming Languages

    Rust 2024 Edition: Async Closures, Gen Blocks, Never Type & Everything New

    Mortex Solutions TeamFebruary 17, 202615 min read
    Rust 2024 Edition: Async Closures, Gen Blocks, Never Type & Everything New

    The Rust 2024 edition is the most feature-rich edition since 2021, introducing long-awaited language features that make async programming, iteration, and error handling significantly more ergonomic.

    Rust programming language

    🦀 Major Features at a Glance

    • Async Closures — async || {} syntax for async closures
    • Gen Blocks — gen { yield value } for easy iterator creation
    • Never Type (!) — Stabilised for exhaustive matching
    • RPITIT — Return-position impl Trait in Traits
    • Unsafe Extern Blocks — Better FFI safety

    1. Async Closures

    Previously, creating async closures required workarounds. Now they're first-class:

    // Before: Awkward workaround
    let fetch_user = |id: u64| {
        let id = id; // move into async block
        async move {
            reqwest::get(format!("/users/{id}"))
                .await?
                .json::<User>()
                .await
        }
    };
    
    // After: Clean async closure syntax
    let fetch_user = async |id: u64| -> Result<User, Error> {
        reqwest::get(format!("/users/{id}"))
            .await?
            .json::<User>()
            .await
    };
    
    // Use with async iterators
    let users: Vec<User> = ids
        .iter()
        .map(async |id| fetch_user(*id).await.unwrap())
        .collect()
        .await;

    2. Gen Blocks — Easy Iterators

    Creating iterators used to require verbose struct + Iterator trait implementations. Gen blocks make it trivial:

    // Before: Manual Iterator implementation (30+ lines)
    struct Fibonacci { a: u64, b: u64 }
    impl Iterator for Fibonacci {
        type Item = u64;
        fn next(&mut self) -> Option<u64> {
            let result = self.a;
            (self.a, self.b) = (self.b, self.a + self.b);
            Some(result)
        }
    }
    
    // After: Gen block (5 lines!)
    fn fibonacci() -> impl Iterator<Item = u64> {
        gen {
            let (mut a, mut b) = (0, 1);
            loop {
                yield a;
                (a, b) = (b, a + b);
            }
        }
    }
    
    // Usage
    for n in fibonacci().take(10) {
        println!("{n}"); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
    }

    3. Never Type (!) Stabilised

    The ! type represents computations that never complete — useful for exhaustive pattern matching and error handling.

    // Function that never returns
    fn exit_with_error(msg: &str) -> ! {
        eprintln!("Fatal: {msg}");
        std::process::exit(1);
    }
    
    // Exhaustive matching with !
    enum Result<T> {
        Ok(T),
        Err(!), // This variant can never be constructed
    }
    
    // The compiler knows Err is impossible, so:
    fn always_succeeds() -> Result<String> {
        Result::Ok("hello".into())
    }
    
    let value = match always_succeeds() {
        Result::Ok(v) => v,
        // No Err arm needed — compiler knows it's unreachable!
    };

    4. RPITIT — Traits with impl Trait Returns

    Traits can now return impl Trait without needing Box<dyn Trait>:

    trait DataSource {
        // Return an iterator — each implementor picks concrete type
        fn records(&self) -> impl Iterator<Item = Record>;
        
        // Return a future — enables async trait methods
        fn fetch(&self, id: u64) -> impl Future<Output = Record>;
    }
    
    struct Database;
    impl DataSource for Database {
        fn records(&self) -> impl Iterator<Item = Record> {
            self.cache.iter().cloned()
        }
    
        async fn fetch(&self, id: u64) -> Record {
            sqlx::query_as("SELECT * FROM records WHERE id = ?")
                .bind(id)
                .fetch_one(&self.pool)
                .await
                .unwrap()
        }
    }

    5. Unsafe Extern Blocks

    // All items in unsafe extern are implicitly unsafe to call
    unsafe extern "C" {
        // Compiler ensures you handle safety correctly
        safe fn strlen(s: *const c_char) -> usize;  // marked safe
        fn malloc(size: usize) -> *mut c_void;       // remains unsafe
    }
    
    // strlen can be called without unsafe block
    let len = strlen(c_string.as_ptr());
    
    // malloc still requires unsafe
    let ptr = unsafe { malloc(1024) };
    🦀 Key Takeaway: The Rust 2024 edition makes the language significantly more ergonomic without sacrificing safety. Gen blocks and async closures alone will transform how you write iterators and async code.