How to write a line to stdout in tokio
A recipe on how to write a line to stdout using tokio's async I/O.
I was trying to figure out how to write a line to stdout in tokio. I knew I needed async I/O since this is a tokio project, but I wasn’t sure what the idiomatic approach was.
It turns out you use tokio::io::stdout() combined with the AsyncWriteExt
trait.
Here’s the basic example:
use tokio::io::{self, AsyncWriteExt};
#[tokio::main]
async fn main() -> io::Result<()> {
let mut stdout = io::stdout();
stdout.write_all(b"Hello, world!\n").await?;
stdout.flush().await?;
Ok(())
}Let me break down what’s happening here.
First, you need to import AsyncWriteExt. This trait provides the utility
methods for async writers:
use tokio::io::AsyncWriteExt;Then you get a handle to stdout using io::stdout(). Note that each call to
stdout() creates a new writer, so you should reuse the same handle if you’re
writing multiple times:
let mut stdout = io::stdout();
stdout.write_all(b"first line\n").await?;
stdout.write_all(b"second line\n").await?;Now, why write_all and not just write? Great question. The write() method
may write fewer bytes than you requested, it returns how many bytes were
actually written, and you have to handle the rest yourself. The write_all()
method guarantees that all bytes are written before returning, retrying as
needed. For most cases, especially when you just want your line to appear,
write_all is the safer choice.
Also notice the \n in the string — tokio doesn’t have a built-in writeln!
macro like std. You have to manually include the newline character:
stdout.write_all(b"My line\n").await?; // \n is the line endingFinally, flush() ensures the output is actually written to the terminal
immediately. Without it, the data might sit in a buffer. For stdout, this
matters less since it’s line-buffered by default in most terminals, but it’s
good practice when you need guaranteed immediate output.
One more tip from the tokio docs: if you’re writing in a loop, create the stdout
handle once outside the loop rather than calling io::stdout() each iteration.
Otherwise, each write goes through a different blocking thread and you might get
mangled output:
let mut stdout = io::stdout();
for message in messages {
stdout.write_all(message).await?;
stdout.flush().await?;
}That’s it! The key takeaways are: use tokio::io::stdout() with
AsyncWriteExt, prefer write_all over write, and remember to add your own
newline character.