Rust nightly features you should watch out for

12th May, 2025

Rust is all about “stability without stagnation”, which is why the nightly branch exists: to allow new features for the language that can be tested before they land in the stable branch. So I decided to go over some of the current nightly features that I find interesting.

Gen blocks

Tracking issue: https://github.com/rust-lang/rust/issues/117078

gen blocks let you write iterators that yield a single value at a time. Manually creating iterators can often be painful and confusing, and mutable iterators that track state are often impossible in safe code. gen blocks offer a more concise and readable alternative.

Consider an iterator that iterates over the Fibonacci sequence, a fairly simple operation. With gen blocks we could express it as:

#![feature(gen_blocks)]

fn fibonacci_iter(count: u32) -> impl IntoIterator<Item = i32>{
	gen move{
		let mut prev = 0;
		let mut next = 1;
		
		yield prev;
		
		for _ in 0..count{
			let curr = prev + next;
			prev = next;
			next = curr;
			yield curr;
		}
	}
}

The equivalent iterator, implemented manually, would look like:

struct FibonacciIter{
	prev: i32,
	next: i32,
	count: u32
}

impl FibonacciIter{
	fn new(count: u32) -> Self{
		Self{
			prev: 0,
			next: 1,
			count
		}
	}
}

impl Iterator for FibonacciIter{
	type Item = i32;
	
	fn next(&mut self) -> Option<Self::Item>{
		if self.count <= 0{
			return None;
		}
		
		let curr = self.next + self.prev;
		self.prev = self.next;
		self.next = curr;
		return Some(curr);
	}
}

Default field values

Tracking issue: https://github.com/rust-lang/rust/issues/132162

This feature allows struct definitions to provide default values for individual struct fields. Those fields can then be left out when initializing the struct.

#![feature(default_field_values)]
struct Player{
	name: String,
	health: u8 = 255,
	damage: u32 = 5
}

let player = Player{
	name: String::from("Player 1"),
	..
}

It’s a fairly simple yet convenient feature. Why not just implement Default? Sometimes you have specific fields that you don’t want to have a default value, such as ids and passwords, while the rest can be left to their defaults.

What happens when you combine default fields with #[derive(Default)]? In that case, your default fields will override type’s default implementation. If we derive default on our above struct we can check to see the output.

#[derive(Default,Debug)]
struct Player{
	name: String,
	health: u8 = 255,
	damage: u32 = 5,
}

let player = Player::default();

dbg!(player.name); // Output: ""
dbg!(player.damage); // Output: 5
dbg!(player.health); // Output: 255

However, you can’t override the default values when manually implementing [Default].

struct Player{
	name: String,
	health: u8 = 255,
	damage: u32 = 5,
}

impl Default for Player{
	fn default() -> Self{
		// This code will raise an error since we have conflicting default values
		Self{
			name: String::new(),
			health: 100,
			damage: 100
		}
	}
}

The default fields are restricted to const values, so all non-const operations will fail to compile.

Inner structs

If you have nested structs, you can provide default values for those fields as well.

#![feature(default_field_values)]

struct BronzeArmour{
	health: u8
}

struct Player{
	name: String,
	health: u8 = 255,
	damage: u32 = 5
	armour: BronzeArmour = BronzeArmour{
		health: 50
	}
}

let player = Player{
	name: String::from("Player 1"),
	..
}

Never type

Tracking issue: https://github.com/rust-lang/rust/issues/35121

The ! (never) type represents a value that never exists at runtime.

use std::process::exit;

#![feature(never_type)]
fn close() -> !{
	// exits the program and never returns
	exit(0)
}

Why would you want to represent a value that never evaluates? Well sometimes you have an operation that never returns or is never valid. Take this example, from the RFC, of the implementation of FromStr for String.

impl FromStr for String{
	type Error = !;
	
	fn from_str(s: &str) -> Result<String,!>{
		Ok(String::from(s))
	}
}

This error can simply never happen, because a &str can always be converted to a String. Which means we can safely unwrap because we are guaranteed by the compiler that the Result will always be Ok.

let r: Result<String, !> = FromStr::from("Hello");
let s: String = r.unwrap();

The current implementation uses the Infallible type as the error, as a placeholder while ! is still unstable. However since it’s just an enum it doesn’t carry the same level of guarantee.

Try expressions

Try blocks allow you to run an operation inside a block and return a Result, since the block returns a result you can propagate any errors inside the block.

#![feature(try_blocks)]
use std::io::Error;

let result: Result<Vec<u8>,Error> = try{
	fs::read("foo.txt")?
}

Just like functions that return a Result, the errors in a try block must coerce into the same type when being propagated.

Try blocks actually originated with the ? operator; they were designed to be used together. In the original rfc the intention was for ? to propagate errors and try{} (originally named catch) to handle errors.

The most important additions are a postfix ? operator for propagating “exceptions” and a catch {..} expression for catching them.

However once propagating errors was implemented, you could simply return a Result from the entire function, which lessened the need for try blocks. They still would be useful, in cases when you want to propagate errors within a specific scope without the entire function returning a Result. That means you’ve handled all propagated errors and the caller can safely use the function without worrying about errors.