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:

Rust
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:

Rust
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:

Rust
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:

Rust
stdout.write_all(b"My line\n").await?;  // \n is the line ending

Finally, 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:

Rust
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.

Other recipes in Tokio