recorded decision · public · no signup
Project Goals 2026
What was chosen
- The `Try` trait will be stabilized, customizing the behavior of the `?` operator for user-defined types beyond `Result` and `Option`.adr ↗
- The Rust project has changed to annual goals rather than six-month goal periods to allow more time for discussion and organization.adr ↗
- Rust's goal process requires contributors to propose goals, and Rust teams then accept them.adr ↗
- The next-generation trait solver will be stabilized to fix type system soundness bugs and unblock new async features.adr ↗
- The Rust project will stabilize support for `cargo script`, allowing a single file to contain both Rust code and a `Cargo.toml`.adr ↗
- The never type `!` will be stabilized, representing computations that never produce a value.adr ↗
- Rust will integrate `const` into the trait system, allowing `const fn` to call trait methods.adr ↗
Constraints
- New Rust Project Goals may be added over the course of the year only if all required resources like champions and funding are already known.adr ↗
Consequences
The recorded why
- Feature Name:
project_goals_2026 - Start Date: 2026-02-23
- RFC PR: rust-lang/rfcs#3935
§ Summary
Establish the initial round of Rust Project Goals for 2026 along with a set of current roadmaps, which describe multi-year development arcs.
New Rust Project Goals may be added over the course of the year but only if all required resources (champions, funding, etc) are already known.
§ Motivation
The 2026 goal slate consists of 66 project goals. In comparison to prior rounds, we have changed to annual goals rather than six-month goal periods. Annuals goals give us more time to discuss and organize.
Why we set goals
Goals serve multiple purposes.
For would-be contributors, goals are Rust's front door. If you want to improve Rust - whether that's a new language feature, better tooling, or fixing a long-standing pain point - the goal process is how you turn that idea into reality. When you propose a goal and teams accept it, you get more than approval. You get a champion from the relevant team who will mentor you, help you navigate the project, and ensure your work gets the review attention it needs. Goals are a contract: you commit to doing the work, the project commits to supporting you.
For users, goals serve as a roadmap, giving you an early picture of what work we expect to get done this year.
For Rust maintainers, goals help to surface interactions across teams. They aid in coordination because people know what work others are aiming to do and where they may need to offer support.
Goals are proposed by contributors and accepted by teams
As an open-source project, Rust's goal process works differently than a company's. In a company, leadership sets goals and assigns employees to execute them. Rust doesn't have employees - we have contributors who volunteer their time and energy. So in our process, goals begin with the contributor: the person (or company) that wants to do the work.
Contributors propose goals; Rust teams accept them. When you propose a goal, you're saying you're prepared to invest the time to make it happen. When a team accepts, they're committing to support that work - doing reviews, engaging in RFC discussions, and providing the guidance needed to land it in Rust.
How these goals were selected
Goal proposals were collected during the month of January. Many of the goals are continuing goals that are carried over from the previous year, but others goal are new.
In February, an alpha version of this RFC is reviewed with teams. Teams vet the goals to determine if they are realistic and to make sure that goal have champions from the team. A champion is a Rust team member that will mentor and guide the contributor as they do their work. Champions keep up with progress on the goal, help the champion figure out technical challenges, and also help the contributor to navigate the Rust team(s). Champions also field questions from others in the project who want to understand the goal.
How to follow along with a goal's progress
Once the Goals RFC is accepted, you can follow along with the progress on a goal in a few different ways:
- Each goal has a tracking issue. Goal contributors and champions are expected to post regular updates. These updates are also posted to Zulip in the
#project-goalschannel. - Regular blog posts cover major happenings in goals.
§ Guide-level explanation
There are a total of 66 planned for this year. That's a lot! You can see the complete list, but to help you get a handle on it, we've selected a few to highlight. These are goals that will be stabilizing this year or which we think people will be particularly excited to learn about.
Important: You have to understand the nature of a Rust goal. Rust is an open-source project, which means that progress only happens when contributors come and make it happen. When the Rust project declares a goal, that means that (a) contributors, who we call the task owners, have said they want to do the work and (b) members of the Rust team members have promised to support them. Sometimes those task owners are volunteers, sometimes they are paid by a company, and sometimes they supported by grants. But no matter which category they are, if they ultimately are not able to do the work (say, because something else comes up that is higher priority for them in their lives), then the goal won't happen. That's ok, there's always next year!
Running Rust scripts will get more convenient with cargo script
| Goal | What and why |
|---|---|
| Stabilize cargo-script | Stabilize support for "cargo script", the ability to have a single file that contains both Rust code and a Cargo.toml. |
People involved: [Ed Page][]
"Cargo script" let's you create a single file that specifies both a Rust program and the dependencies it needs and then execute that program with one convenient command. For example, you can now take a Rust file like this:
#!/usr/bin/env cargo
---
edition: 2024
[dependencies]
reqwest = { version = "0.12", features = ["blocking"] }
---
fn main() {
let body = reqwest::blocking::get("https://httpbin.org/ip")
.unwrap()
.text()
.unwrap();
println!("My IP info: {body}");
}
and run it with cargo my_ip.rs. Or, thanks to the #! line, you can just run ./my_ip.rs.
This feature makes good use of one of the things we found when doing our research for the Vision Doc: that people love Rust not only because it helps them build foundational software, but because it's a expressive and productive enough that "you can write everything from the top to the bottom of your stack in it" (-- Rust expert and consultant focused on embedded and real-time systems). Until now, the fly in the ointment was that packaging up a Rust package required several files and required people to do a separate compilation step. Cargo script solves that problem.
The borrow checker will be more flexible with Polonius alpha
| Goal | What and why |
|---|---|
| Stabilize polonius alpha | Fix remaining issues, validate on real-world code, and ship a stable improved borrow checker. |
| Extend formality for Polonius alpha | Build a formal model of borrow checking in a-mir-formality and upstream it into the Rust reference. |
People involved: [Amanda Stjerna][], [Jack Huey][], [Rémy Rakic][], [Niko Matsakis][], [tiif][]
The "Polonius Alpha" work represents the final completion of the original promise from the 2018 Non-lexical Lifetimes RFC. That RFC originally planned to address three problematic patterns -- but ultimately, for efficiency reasons, we were only able to fix two. In the meantime, for the last several years, we have been pursuing work on Polonius, a next generation borrow checker formulation, that aims to close this gap and more.
The Polonius Alpha rules extend the borrow checker to accept the so-called "Problem Case #3" that NLL ultimately failed to solve. This case occurs when you have conditional control flow across functions. For example, in this case the call to map.get_mut(&key), the borrow of map is only "live" in the Some branch, where it is returned (and hence must outlive 'r). But because of imprecision in the borrow checker, the borrow winds up being enforced in the None branch as well, resulting in an error:
fn get_default<'r,K:Hash+Eq+Copy,V:Default>(
map: &'r mut HashMap<K,V>,
key: K,
) -> &'r mut V {
match map.get_mut(&key) { // ──────────────────┐ 'r only needs to
Some(value) => value, // ◄────┘ be valid here...
None => { // │
map.insert(key, V::default()); // │
// ^~~~~~ ERROR // │
map.get_mut(&key).unwrap() // │
} // │
} // │ ...but today it covers
} // ◄────┘ all this
Under Polonius Alpha, this code compiles.
Polonius Alpha is part of a larger roadmap called the Borrow-Checker Within that we expect to be driving over the next few years. This year, another part of that work is including Polonius Alpha in a-mir-formality, the types team's (in-progress) specification for how the Rust type system works. As part of another goal, we are planning to integrate a-mir-formality into the Rust reference. This would make Polonius the first version of the borrow checker whose behavior is specified outside of the Rust compiler.
Change const evaluation to support traits, and reflection, allow structs/enums as const parameter types
| Goal | What and why |
|---|---|
| ADT const params | Support structs, tuples, arrays in const generics. |
| Min generic const arguments | Support associated constants and generic parameters embedded in other expressions. |
| Stabilize const traits MVP | Finalize the RFC, complete the compiler implementation, and stabilize so const fn can call trait methods. |
Explore design space for comptime const fn | Implement and validate #[compile_time_only] attribute for const fn that enables type reflection without runtime overhead. |
People involved: [Boxy][], [Deadbeef][], [Josh Triplett][], [Niko Matsakis][], [Oliver Scherer][], [Scott McMurray][], [TC][]
This year we'll be extending Rust's support for const evaluation in several ways. To start, you'll be able to use structs and enums as the values for const generics, not only integers. So where today you can write Array<3>, you'll be able to write something like this:
pub struct Dimensions {
pub width: u32,
pub height: u32,
}
pub fn process<const D: Dimensions>(data: &[f32]) {
// ...
}
fn main() {
process::<{ Dimensions { width: 1920, height: 1080 } }>(&data);
}
You'll also be able to use associated constants as const generic arguments, like Buffer<T::MAX_SIZE>.
Next, we are integrating const into the trait system. When you implement a trait, you'll be able to provide a const impl which means that the methods in the trait are all const-compatible. const fn can then use bounds like T: const Display to indicate that they need a type with a const-compatible impl or T: [const] Display to indicate that they need a const-compatible impl when called in a const context. Const traits are particularly helpful because they allow you to use builtin language constructs like ? and for loops:
const fn sum_up<I: [const] Iterator<Item = i32>>(iter: I) -> i32 {
let mut total = 0;
for val in iter {
total += val;
}
total
}
Finally, we're beginning early experimental work on compile-time reflection — the ability for const functions to inspect type information. It's too early to promise specifics, but the long-term vision is things like serialization working without derive macros.
Ergonomic ref-counting and (maybe) async traits
| Goal | What and why |
|---|---|
| Box notation for dyn async trait | Enable dyn dispatch for async traits via .box notation |
Add a Share trait | A trait that identifies types where cloning creates an alias to the same underlying value, like Arc, Rc, and shared references. |
Support move(...) expressions in closures | Precise control over what closures capture and when, eliminating the need for awkward clone-into-temporary patterns. |
People involved: [Takayuki Maeda][], [Niko Matsakis][], [Santiago Pastorino][]
We have a lot of ongoing plans to improve the async Rust experience, but the two most likely to hit stable are more ergonomic ref-counting and extensions to async fn in traits.
The ergonomic ref-counting discussion has gone through many stages, but one solid step everyone agrees on is making it (a) more obvious when you are sharing two handles to the same object vs doing a deep clone, via the Share trait, and (b) more ergonomic to capture clones into closures and async blocks with move($expr) expressions:
// Today: awkward temporary variables
let tx_clone = tx.clone(); // am I deep cloning or what?
tokio::spawn(async move {
send_data(tx_clone).await;
});
// With Share + move expressions: inline and clear
tokio::spawn(async {
send_data(move(tx.share())).await;
}); // ---------------- capture a shared handle
We also plan to cut a "practical path" to support invoking async fns through dyn Trait. The initial version would be limited to boxed futures but the goal is to be forwards-compatible with the ongoing in-place initialization designs for non-boxed allocation (e.g., stack). The RFC for this hasn't been written yet, and the proposal includes some new syntax, so that could be spicy! Stay tuned.
Try, never, extern types, oh my!
| Goal | What and why |
|---|---|
| Sized Hierarchy and Scalable Vectors | Over the next year, we will build on the foundational work from 2025 to stabilize the Sized trait hierarchy and continue nightly support for scalable vectors: |
Stabilize never type (!) | Stabilize the never type aka !. |
| Stabilize the Try trait | Stabilize the Try trait, which customizes the behavior of the ? operator. |
People involved: [Amanieu d'Antras][], [waffle][], [David Wood][], [Jana Dönszelmann][], [lcnr][], [Niko Matsakis][], [Tyler Mandry][]
Three long-awaited features are making their way toward stabilization this year.
The Try trait customizes the behavior of the ? operator, letting you use it with your own types beyond Result and Option. For example, you could define a TracedResult that automatically captures the source location each time an error is bubbled up with ?:
fn read_list(path: PathBuf) -> TracedResult<Vec<i32>> {
let file = File::open(path)?; // captures location
Ok(read_number_list(file)?) // captures location
}
No more choosing between readable error handling and useful diagnostics.
The never type ! has been unstable for ten years. It represents computations that never produce a value — like functions that always panic or loop forever. The final blockers are being resolved, and stabilization is in sight.
Finally, the Sized trait hierarchy work will stabilize a richer set of sizing traits, which unblocks extern types — another long-requested feature. Today, ?Sized conflates "unsized but has metadata" with "truly sizeless." The new hierarchy distinguishes these cases. This same work is also laying the foundation for scalable vector support (Arm SVE), where vector sizes depend on the CPU rather than being fixed at compile time.
Going "beyond the &" with better integration for custom pointer types
| Goal | What and why |
|---|---|
| Arbitrary self types | Let user-defined smart pointers work as method receivers, stabilizing the arbitrary_self_types feature. |
Deref/Receiver split chain experiment | Experiment with letting Receiver::Target and Deref::Target diverge, collecting data on utility and use cases. |
derive(CoercePointee) | Support dyn Trait coercion for user-defined smart pointers. |
| Map the field projection design space | Explore the virtual places approach, document it in the beyond-refs wiki, formalize borrow checker integration, and build a compiler experiment. |
People involved: [Benno Lossin][], [Alice Ryhl][], [Mario Carneiro][], [Ding Xiang Fei][], [Jack Huey][], [Niko Matsakis][], [Oliver Scherer][], [Tyler Mandry][], [TC][]
Three goals this year are working to make it possible for user-defined types to be used in all the ways that you can use Box, Arc, and &.
Arbitrary self types lets you use custom smart pointers as method receivers. With the Receiver trait and derive(CoercePointee), your pointer types work just like Box or Arc — including method dispatch and coercion to dyn Trait:
impl Person {
fn biometrics(self: &SmartPointer<Self>) -> &Biometrics {
...
}
}
let person: SmartPointer<Person> = get_data();
let bio = person.biometrics(); // just works
Reborrow traits allow custom pointers to be reborrowed, just like mutable references. When working with Pin, for example, you should no longer have to call pinned_ref.as_mut() to fix lifetime issues.
We are also continuing our experimental work to support custom field projections — accessing fields through a smart pointer. Today, &x.field gives you &Field, but there's no equivalent for NonNull, Pin, or custom pointer types. The field projections design is exploring a "virtual places" approach that would make this work generically. The goal for this year is a compiler experiment on
Excerpt — the full document is at the cited source: text/3935-Project-Goals-2026.md ↗