How to Write an RFC for Rust

Write an RFC by proposing a feature in the rust-lang/rfcs repo, discussing it, and creating a tracking issue in the main rust repo for implementation and stabilization.

You hit a wall in the standard library

You've been building a high-performance allocator for an embedded system. You need to handle out-of-memory conditions gracefully. Vec::push panics if allocation fails. You sketch a try_push method that returns a Result. You prototype it in a fork of the compiler. It works. You can't just merge it.

Rust is a shared resource. Every addition to the standard library changes the surface area for every user, forever. The language team requires a Request for Comments (RFC) before any change to the language, compiler, or standard library. This process ensures changes are well-justified, thoroughly discussed, and implemented correctly. It's not bureaucracy. It's quality control. The RFC process is the immune system of the language. It rejects bad ideas and polishes good ones.

The blueprint for a change

An RFC is a structured proposal. It forces you to answer specific questions before anyone writes implementation code. The process has distinct phases: discussion, acceptance, implementation, and stabilization. You propose a change, the community critiques it, you refine it, and eventually, the teams give a green light. Once accepted, you implement the feature behind a feature gate. Users can opt-in on nightly. After testing and feedback, the feature stabilizes and becomes part of stable Rust.

Think of an RFC like a zoning proposal for a city. You don't just start pouring concrete for a new skyscraper. You submit plans to the city council. You hold public hearings. Residents point out traffic issues. Engineers check the foundation. You revise the plans. Once the council approves, you get a permit. In Rust, the city is the language ecosystem. The council is the community and the teams. The permit is the RFC acceptance. The construction is the implementation. The occupancy certificate is stabilization.

The RFC template is your blueprint. It lives in the rust-lang/rfcs repository. Every RFC follows the same structure. This isn't arbitrary. Each section forces you to confront a specific aspect of the design. If you can't fill a section, the idea isn't ready.

// This is the structure of an RFC.
// Treat it like a checklist for your own sanity.
// If you skip a section, reviewers will assume you haven't thought it through.

// Title: Clear, descriptive, no marketing fluff.
// Example: "Add try_push to Vec for fallible allocation"
// NOT: "Super cool allocation feature"

// Summary: One paragraph. What is this?
// Keep it tight. A busy reviewer should get the gist in 10 seconds.

// Motivation: Why do we need this?
// Show the pain point. Include code that fails or is awkward.
// Explain why existing workarounds are insufficient.

// Detailed Design: How does it work?
// Syntax, semantics, edge cases, interaction with existing APIs.
// This is the core of the RFC. Be precise.

// Drawbacks: Why might this be a bad idea?
// API bloat? Compile time impact? Cognitive load?
// If you write "No drawbacks", the RFC will likely be rejected.

// Alternatives: What else did you consider?
// Why is this approach better than the alternatives?

// Unresolved Questions: What's still fuzzy?
// List open items. The RFC can be accepted with unresolved questions,
// but they must be resolved before stabilization.

Convention aside: The Drawbacks section is where most RFCs fail. If you write "No drawbacks", reviewers will assume you haven't thought deeply enough. Every feature has costs. API surface area, compile time, cognitive load, backwards compatibility. List them. Defend your design against them. A strong RFC acknowledges the costs and argues that the benefits outweigh them.

Walking through a proposal

Let's walk through a realistic example. You're proposing Vec::try_push. You draft the RFC. The Summary is concise. Adds a fallible push method to Vec for allocation failure handling. The Motivation shows the pain. You include code snippets where push panics and explain why catching panics is wrong for control flow. You show the desired API.

The Detailed Design covers the return type. You choose Result<(), AllocError>. You discuss why not Option. You discuss interaction with try_reserve. You list Unresolved Questions. Should this be in a trait? Should it work for VecDeque? You open a PR in rust-lang/rfcs.

The community responds. Comments fall into two categories. Nits are small fixes. Typos, formatting, minor wording. Substantive comments challenge the design. Why not use a crate? Does this encourage error handling patterns we don't want? You address every comment. You update the PR. You keep the conversation moving. This can take weeks or months. Patience is part of the process.

Eventually, a team member starts a Final Comment Period (FCP). This is the vote. It usually lasts a week or two. Team members review the RFC and the discussion. They vote to accept or reject. If accepted, the RFC is merged. If rejected, you can iterate and resubmit, or drop it. The decision is final for that version of the proposal.

Convention aside: Use @rustbot modify labels: +T-libs to route your PR to the right team. The rustbot automation helps manage the workflow. Learn the commands. It speeds up the process. The bot tracks the state of the RFC, the FCP, and the voting. It's your ally.

From acceptance to stabilization

Once accepted, you create a tracking issue in rust-lang/rust. The tracking issue is the source of truth for implementation. It lists the steps: implementation, documentation, tests, stabilization. You implement the feature behind a feature gate. #[feature(vec_try_push)]. This allows nightly users to test the feature. If something breaks, the feature gate protects stable users.

// Implementation in rust-lang/rust.
// The feature gate is mandatory.
// Without it, the compiler will reject the code on stable.

#[unstable(feature = "vec_try_push", issue = "12345")]
impl<T> Vec<T> {
    /// Attempts to push an element, returning an error if allocation fails.
    ///
    /// # Errors
    ///
    /// Returns an error if the allocation fails.
    pub fn try_push(&mut self, value: T) -> Result<(), AllocError> {
        // Implementation details...
        // SAFETY: We handle allocation failure explicitly.
        // The caller is responsible for handling the error.
        // We do not panic.
        if self.len == self.cap {
            // Try to grow...
            // If growth fails, return Err.
        }
        // ...
    }
}

You write tests. You update the docs. You ping the tracking issue when you're ready for stabilization. The team reviews the implementation. They check the tests. They verify the docs. If everything looks good, they merge a stabilization PR. The feature gate is removed. The feature becomes part of stable Rust.

Convention aside: The tracking issue is the heartbeat of your feature. Keep it updated. Add comments when you make progress. Link to PRs. If the issue goes stale, the team may close it. Momentum matters.

Pitfalls and compiler errors

Scope creep is the enemy. You start with try_push and end up proposing a new error handling system. Keep the RFC focused. If the scope grows, split it into multiple RFCs. Backwards compatibility matters. If your change breaks existing code, you need a strong justification. Prefer additive changes. no_std awareness is required. If your feature touches the standard library, it must work in no_std environments. If it can't, explain why and document the limitation. Ignoring no_std will get your RFC rejected.

When you implement the feature, you wrap it in #[unstable(feature = "vec_try_push", issue = "12345")]. If a user tries to use it on stable, they get E0658 (unstable feature). This is the gate. It tells the user the feature is experimental. The error message links to the tracking issue. This connects the user to the discussion. It's a feedback loop. Users can report bugs, suggest improvements, and provide real-world usage data.

Another common error is E0432 (unresolved import) when users try to import a type that's behind a feature gate. The compiler helps here. It tells the user to enable the feature. Make sure your feature name is descriptive. vec_try_push is clear. vtp is not. Descriptive feature names help users understand what they're opting into.

Treat the tracking issue as the source of truth for implementation status. If the issue is closed, the feature is dead. Keep it alive.

When to write an RFC

Write an RFC when you want to change the language syntax or semantics. Write an RFC when you want to add a type or function to the standard library. Write an RFC when you want to change compiler diagnostics or error codes. Write an RFC when you want to change the stability guarantees of existing APIs.

Reach for a crate when the feature is useful but doesn't belong in the standard library. Reach for a macro or proc-macro when the feature can be implemented as user-space code without compiler changes. Reach for an issue in rust-lang/rust when you found a bug or have a small documentation fix. Reach for a library team RFC when the change is specific to a sub-team's domain and doesn't affect the wider language.

Use a crate for domain-specific logic. The standard library is for general-purpose utilities. If your feature is only useful for a specific domain, it belongs in a crate. Use a macro for code generation. If your feature can be implemented as a macro, it doesn't need an RFC. Macros are powerful. They can solve many problems without changing the compiler. Use an issue for bugs. If you found a bug, open an issue. Don't write an RFC for a bug fix. Use a library team RFC for sub-team changes. If your change only affects std::io or std::net, you might only need a library team RFC. Check the team's guidelines.

The RFC process is slow by design. Speed leads to regret. Take the time to get it right.

Treat the RFC template as a contract with the community. If you can't write the drawbacks, you don't have an RFC. Stabilization is the finish line, but the race is won in the RFC.

Where to go next