---
title: New `write!` design
tags: ["T-libs-api", "design", "brainstorming"]
date: 2025-10-02
url: https://hackmd.io/xb-b_q4FQDKkwZEGQSaQwA
---
# New `write!` design
Attendees: Amanieu d'Antras, Josh Triplett, Tomas Sedovic
Meeting notes: Tomas Sedovic
Amanieu: Trying to find a sane way to redo the `write!` macro and formatting in general.
Amanieu: We have the `write!` macro (and related like `writeln!`) and two traits: `fmt::Write` and `io::Write`. Both return different `Result` types. I want to see how `io::Write` works because I want to know how does it do the conversion.
Josh: `fmt::Write` is available in `core` and `io::Write` is only available in `std` because of `io::Error`. We've talked about having an associated type for errors, and that might help. The new thing should hopefully be available in `core`.
Josh: `write_fmt` in `std::io::Write`
Aamnieu: but the io::write version needs to be able to convert from `fmt::Result` to `io::Result` because that's what `format_args!` expects.
Josh: But nothing needs to convert from `io::Result` to `fmt::Result`. `write!` on an `io` checks if the underlying device produced an error and if it does, it panics.
Amanieu: Riht, so it keeps track of the io::Error separately. We can do that more generally.
Josh: Yeah, we could do that with a generic.
Amanieu: I wonder if this is going to have to happen over an edition.
Josh: It will.
Amanieu: We have to introduce a new "write" macro.
Josh: And have both editions have it under different names and then change what `write` means over an edition.
Josh: Or we could ask everyone to migrate to the new one. But that could be a huge migration across the ecosystem. Especially if we change the macro name e.g. to `w!`.
Josh: Also, I rarely do `.unwrap`, I mostly do question mark.
Amanieu: Wishlist: if you write into a String or Vec, it returns `()`.
Josh: people regularly ask us to return an allocation error. If you do this on a Vec, it returns nothing. But you should be able to write a wrapper over Vec that returns an allocation error.
Amanieu: We can defer that.
Josh: Agreed. I'm saying the architecture should be such that you could write this. I think we both agree that we want an associated type for Error.
Amanieu: `core::fmt::write` uses the `fmt::Write` adaptor.
Josh: Right, so that one is hardcoded to fmt::Write.
Amanieu: And `io::write` wrapper uses this.
Josh: And then panics over an error.
Josh: Should we also have a new `Display` trait? In particular one that's not allowed to error?
Amanieu: If the output sync errors, we need to abort displaying stuff or exit.
Josh: That's an argument that `WriteFmt` should have the option to error. Not an argument that Display should.
Amanieu: Display should error if write_str errors.
Josh: Let's assume we'll keep fmt::Error in Display. Then, should we allow for the possibility that fmt::Error is produced in the underlying stream?
Josh: One option is whatever our new trait is, it could have some way of translating fmt::Error to its error type.
Josh: Another option is you're not allowed to do that unless the underlying stream fail. If it does, you get a real error and you just capture that error. Do what `io::WriteFmt` and when `Display` would panic if the underlying error happens. We don't propagate the fmt::Error.
Amanieu: I don't think we want to support fmt::Error, that's an user error.
Initial draft spec of a trait:
```rust
pub trait WriteFmt {
type Error;
fn write_str(&mut self, s: &str) -> Result<(), Self::Error>;
}
```
Amanieu: Suppress `must_use` for `Result<(), !>`, including if the `!` comes from an associated type where the concrete implementation sets it to `!`.
Josh: Agreed. If you have `Result<(), !>` there's no alternative. An alternative is that we could say that you can call `?` and it will do nothing.
Amanieu: What does the `From(!)` call?
Josh: We have an `impl From<!> for Infallible` and some other types. There's a mention of `impl From<!> for T`. But needs checking with the compiler team and may depend on the trait solver.
Amanieu: If `must_use` is suppressed, you don't need the question mark at all.
Josh: I agree. You would only need it in a generic context.
Josh: I think we should just make `must_use` be suppressed. `!` is to the best of my knowledge already a lang item. And assuming `Result` is a lang item, I expect implementing this is a quick hard-coded thing where it check for `()` and `!` and suppress that.
Amanieu: We may have to rename the write_format method so it doesn't conflict with the one on this trait.
Josh: There's no reason not to on the basis that we're providing a new macro. I'd call the trait `WriteFmt` and the method `write`.
Amanieu: `write` could conflict with stuff. This won't be in the prelude because the macro will call the trait specifically.
Josh: Unless you implement iosync.
Amanieu: Is there a reason we can't just have a single-method `write_str`? The others are generated methods but we don't need those.
Josh: In the implementation of `write!` do the handling of the arguments?
Amanieu: This is the sink. The sink hole will need to write strings.
Josh: Right! We don't agregate everything into a string first, we write out the individual pieces. So there is a currently a write_char method. And I think that's an optimization for `write_ln` to write `\n`. With the new work that Mara did to allow you to efficiently combine arguments, I don't think this is needed for efficiency. So I'd say don't add it unless it has to.
Amanieu: I see some code calling it but they're calling it on the formatter. So that's fine.
Josh: To clarify: we're writing a formatter on the assumption that we should never handle partial writes. Because the success case is `()` and that's correct. I want to make sure we don't want to say we've only wrote this much of the string. I want to check we always call `write_all`.
Amanieu: `write_str` docs say it can only succeed if everything was written.
Josh: We don't need write_fmt, because we can do that work ourselves. We should be calling a method to do write_fmt because we don't want to inline it to every single caller.
Amanieu: Right.
Josh: Does anyone override the implementation of `fmt::WriteFmt`? Looks like a fair bit of the standard library does. ... Oh! That's the WriteFmt for io::Write. A lots of people overwrite io::Write.
Amanieu: Why do they override it?
Josh: A lot of people use it to handle EBADF. That says "if it's a bad file descriptor, then silently succeed". So it's meant to handle closed stdio.
Amanieu: Strictly speaking it's not necessary for this one to be overridden.
Josh: The call to write_all should handle this.
Josh: Other thant the "I'm empty, skip the formatting entirely" can you think on any other usecase?
Amanieu: Early exit on the first write to bad FD
Josh: Write to forward to multiple sinks and format it once. But I'd say don't forward to WriteFmt, format to io.
Josh: If we really wanted it, we could do a doc_hidden and we could overwrite it for empty.
Amanieu: There's something called "line_writer_shim" that overrides it. It says it's more efficient than just doing it with `write` and `flush`.
Josh: The implementation for io::Write for writer_shim doesn't provide WriteFmt.
Amanieu: But line_writer_shim does. It uses the default implementation of WriteFmt.
Josh: I don't get why that's more efficient, then.
Josh: Let's for now assume we don't need to make it possible to override. If we decide it later on, it's to add a trait method that's unstable so you can't override it in stable code.
Josh: It would only conflict with `fmt::Write` -- and that doesn'st have to be in the new prelude. It provides a WriteStr.
Amanieu: I think the issue here is: if I have a crate that provides an io sink. What traits do I implement?
Josh: I think we can trivially provide an adaptor that implements WriteFmt using either io::Write or fmt::Write and in both cases it would define the associated type for this error type.
Amanieu: Is that what you want? If I'm exposing e.g. an array string, I will want to implement the FmtWrite trait for compatibility. I can't use a blanket impl.
Josh: I meant a wrapper, not a blanket impl. I can think of two answers here:
1. We say this is a bit of a speed bump when you do an edition bump. If you don't use the trait, you either use one of the new wrappers or you use the old write call.
2. We come up with some way to do an overridable blanket impl. We should be doing `Deref` specialization in the `write!` macro.
Amanieu: We don't want the new macro to be duck typed. It needs to specifically refer to this trait.
Josh: It will specifically refer this trait, but if it doesn't implement it, it could fall back to the other trait.
Amanieu: Will this completely break if the type will have a `write_fmt` method?
Josh: I'm suggesting our specialization is
Amanieu: This adapter will be only for the new macro?
Josh: Correct. I think you could technically do it for the old macro and have the last fallback be the duck typing. But I don't think we should.
*Josh started sketching a solution in the Playground*
Amanieu: The plan is to keep the `write!` macro the same and focus on
Amanieu: Can we do it in `core` that doesn't reference `io::Write`?
Josh: I can! It's powered off of trait impls and we have coherence domain across core and std. If you use core::write and you have std in scope it'll use io::write. And if you don't have std in scope, it won't work. So yes, I can make this work.
Josh: Assuming I can make this work, other than that, there's bikeshedding. What else?
Amanieu: open questions: Do we provide an overridable `write_fmt` methods on the trait? I think we've pretty much settled the design of the associated error types.
Amanieu: The existing write macro: do we want the ability to support the new trait?
Josh: We could do this with a wrapper.
Josh: Should there be any type constraints on the type error here? Should it have to implement Error?
Amanieu: No.
Josh: Should it be static?
Amanieu: No.
Josh: io::Error can wrap an arbitrary error type. And to do that, the type needs to implement Error, Send and Sync. I don't think we need it on the base trait, but to build the wrapper it will have to constraint the error type. But that's okay.
See the type constraints on
https://doc.rust-lang.org/std/io/struct.Error.html#method.new
Amanieu: Yes.
Josh: So we could easily provide wrappers for write_fmt case and io_fmt case. I don't think we can provide a wrapper for an ??
Josh: We could have another new trait that has a write_fmt method and a blanket impl for this one. Anybody could do that, not just us.
Amanieu: Does it make sense to have a blanket impl for FmtWrite? No, it would cause
Amanieu: I think this is enough to start working on a prototype.
Josh: I agree. I'll put an implementation together for the `Deref` bits.
Amanieu: The final open question: are we happy to commit to this somewhat whitespread ecosystem change even though it's not a breakage? I think it's making the language better.
Josh: I think it absolutely makes the language better. There's a net improvement for the people on the new edition. The edition can call the old macro. If we do the obvious translation where we translate "write" to "write_2025" during the edition. This will be a noisy part where a bunch of people's ifs will turn into matches because match has different scoping rules. People complained about that really bitterly. But it's not the end of the world.
Amanieu: In theory during the edition transition, your code should keep working.
Josh: Assuming I can build the prototype, what is the next step? This is not an ACP, this is an RFC for the Libs team.
Amanieu: Yeah, this will have a significant breakage. It needs an RFC.
Josh: I'll take care of the deref specialization. Can you figure out how over an edition we can put write? Do we want to bother making core::fmt::write! work as expected or just tell people to use the one in the prelude?
Amanieu: Do it like we do with panic!. That's edition dependent.
Josh: You're right, we'll do exactly what we do with `panic!` over the edition.
## Notes from Libs-API meeting 2025-09-25
**(new change proposal) rust.tf/libs651 *ACP: Add API to write formatted data directly into a \`Vec\<u8\>\`***
The 8472: I think there's a bunch of related proposals
Amanieu: What needs to happen is io::Write needs to be a subtrait of fmt::Write. Anything you can write bytes into we can write a string into. I don't know how to do that in a backwards-compatible way. Also fmt::Write has a different error type.
Josh: If we had suffix macros that would be convenient for this. But without that adding another Write variant seems not great.
Amanieu: `FmtBuffer` is bad.
Amanieu: If we implement fmt::Write for `Vec<u8>`,what happens?
The 8472: related ACP: https://github.com/rust-lang/libs-team/issues/133
Josh: Today may import fmt::io::Write and fmt::Write, there's no ambiguity.
Amanieu: I wish we had designed the whole thing differently but I don't know how to do this in a backwards-compatible way.
Josh: If we assume we can't do this using the `write!` macro in a backwards compatible way, what is the least awful way to implement it?
The 8472: What's so bad about a wrapper type if you can wrap it around a `&mut` and a tuple constructor?
Josh: Even without the tuple constructor you can have a ?? method ??
The 8472: I was thinking about a more general way to provide io::Write. I linked the past proposal. The only change I would make to make it a tuple constructor to have a shorthand.
Josh: Call it something like `fmt::Writer` and give it a tuple constructor? +1 on that.
Amanieu: I'd really like to try and find a better solution for the `write!` macro.
Josh: Keep in mind we have editions. We can change the macro in an edition. We can't change the trait.
Amanieu: Proposal: we have a single trait. The destination of the `write` macro must implement this one trait. That trait means you can write bytes into it. This trait has an associated error type. And has a write_str method. Takes some bytes and returns a result of unit of associated error type.
Josh: I assume it does write_all so it doesn't have to deal with partial writes?
Amanieu: yes.
Amanieu: Then something like a string or vec could implement the trait with error type of `!` whereas something like io would implement an error.
The 8472: Wouldn't the problem be you pay string validation costs when writing into a string? The interface takes bytes but in practice you write strings.
Amanieu: It could take str because this trait is used for string formatting.
Josh: There is a separate thing where it would be nice to have a macro for binary formatting. But that's the question for another day.
Amanieu: That means you don't need the Write trait anymore when writing the macro.
Josh: We wouldn't necessarily have to have blanket impls for both io::Write and fmt::Write but we could have a blanket impl for one of them.
Amanieu: I would love to have the impls for both. And that's the thing I haven't figured out yet.
Amanieu: Any Display impl can chose to emit an error.
Josh: But in practice we never expect it and wouldn't cope with it.
Josh: I think we want the blanket impl to go in the other direction. But that would absolutely break the universe. You'd have the blanket impl go from this new thing to std::fmt::write. The obvious thougt would be we give this thing a new name and expect the ecosystem to migrate to it? We could call it `W` instead of `Write`.
It would be a massive migration to the ecosystem but doable?
Amanieu: We could provide a wrapper type that takes a reference to a fmt::Write and maps it into the new trait. Or the other way around.
Josh: Yeah, we could. And you don't need the adaptor if you provide a direct impl yourself.
Josh: IT seems there are 2 separate things here: 1. ambiguity between io::Write and fmt::Write. And 2. is the associated error type.
Amanieu: correct.
Josh: We could solve one of the problems over an edition. We could say we always use fmt::Write. And if you want the `write` method, you have to use `fmt::Write`
Amanieu: yes.
Josh: The other one is: how horrible would be to migrate `fmt::Write` to have the associated error type. Can we make that migration compatibly. What if the `write` argument grew an optional error type defaulting to fmt error. But you could implement it with other things including infallible.
Josh: Sorry, that wouldn't work because we don't want a generic but an associated type.
Amanieu: But even the associated type would have issues.
Josh: Under what circumstances anyone errors in fmt::error.
Amanieu: Never. The documentation says you should never do this. The standard library never does this.
Josh: The only time we provide a formatting error in the standard library would be if a Display impl outside of standard library would retourn an error.
Josh: and fmt::Error is opaque, no?
Amanieu: No, it's a unit struct.
(discussion to be continued elsewhere?)
For the actual ACP: we don't have an answer yet. We should pursue the answer in another meeting.
Amanieu: We could have a small meeting to get the initial stuff out.
The 8472: We can also start a zulip thread to have other people to contribute.
Josh: There does't seem to be a big design space here. We know what we want to do, the main issue is compatibility.
The 8472: Could we have a wrapper type without designing the migration?
Josh: Can we respond to https://github.com/rust-lang/libs-team/issues/133 say we want FmtWriter and the tuple struct?
Amanieu: I'd rather wait for having a clear design on how we're going to move forward before replying.
Separate meeting: Josh, Amanieu.
TODO: Tomas can schedule. (Josh: had a lot of luck using cal.com -- if multiple people all use cal.com, you can open a URL for a cal that uses this person + this person + this person it'll find a use time when all people are available)