Vibe Engines
YouTube

Lesson 5.1 · 7 min

Bundling fields: struct

Chatterbox keeps passing around an author in one string and the text in another, and hoping they stay together. A struct gives them one name and one owner.

Adds to ChatterboxA Message struct.

Two strings that belong together

Chatterbox v0.4 records every line into a transcript. But a line has two facts: who said it, and what they said. Today those travel as two loose strings, and nothing stops you from handing the bot's text to the wrong author.

A struct is a named bundle of fields. You decide the fields once, and from then on a Message is one thing you can pass around, store and borrow.

Define the shape once, then build values of it
struct Message {
author: String,
text: String,
}
 
fn main() {
let m = Message {
author: String::from("you"),
text: String::from("hello"),
};
println!("{}: {}", m.author, m.text);
}
$ cargo run
you: hello

The first four lines describe the shape: a Message has an author and a text, both String (lesson 2.4). Inside main we build one by naming every field. Then m.author and m.text read a field with a dot.

Every field must be given. Leave one out and the compiler stops you, which is the point: a Message with no author cannot exist.

A field left out
struct Message {
author: String,
text: String,
}
 
fn main() {
let m = Message {
author: String::from("you"),
};
println!("{}", m.author);
}
error[E0063]: missing field `text` in initializer of `Message`
 --> src/main.rs:7:13
  |
7 |     let m = Message {
  |             ^^^^^^^ missing `text`

E0063 says "missing field text". Not a warning, not a default of empty: the program does not build until the shape is complete.

A struct owns its fields

Remember the one-key model from chapter 4. Each String has exactly one owner. Inside a Message, the message owns its author and its text. When the message goes away, its fields go with it. Move the message and you move everything inside it in one go.

That is why record below takes &Message: it only needs to look, so it borrows. The caller keeps the key.

PredictRead the fields
struct Message {
author: String,
text: String,
}
 
fn main() {
let m = Message {
text: String::from("ping"),
author: String::from("bot"),
};
println!("{}: {}", m.author, m.text);
}

What does this print?

  • bot: ping
  • ping: bot
  • it does not compile
Reveal the answer

The program prints: bot: ping Fields are matched by name, not by position. Writing text first does not swap them. That is a quiet gift of structs: you cannot mix up two strings of the same type.

src/main.rs: Chatterbox v0.5, now with Message
struct Message {
author: String,
text: String,
}
 
fn reply(text: &str) -> String {
format!("You said: {text}")
}
 
fn record(log: &mut String, msg: &Message) {
log.push_str(&msg.author);
log.push_str(": ");
log.push_str(&msg.text);
log.push('\n');
}
 
fn main() {
let mut log = String::new();
for line in ["hello", "how are you"] {
let msg = Message { author: String::from("you"), text: String::from(line) };
record(&mut log, &msg);
let answer = Message { author: String::from("bot"), text: reply(&msg.text) };
record(&mut log, &answer);
}
print!("{log}");
}
$ cargo run
you: hello
bot: You said: hello
you: how are you
bot: You said: how are you

The highlighted lines are the change. record used to take two strings and now takes the bundle. Note the two borrows in main: &mut log to change the transcript, &msg to only look. The loop uses a fixed list of lines so you can run it here; the real program still reads your typing.

Try itYour turn: describe a message

Write describe so it returns the author, a colon, a space and the text, like bot: hi. It only borrows the message.

Write describe so it returns the author, a colon, a space and the text, like bot: hi. It only borrows the message.

struct Message {
author: String,
text: String,
}
 
fn describe(msg: &Message) -> String {
// return "<author>: <text>"
String::new()
}
 
fn main() {
let m = Message { author: String::from("bot"), text: String::from("hi") };
println!("{}", describe(&m));
}

Hidden tests (3): author_comes_first empty_text_still_has_the_colon bot_says_hi

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does a struct give you that two loose strings do not?Reveal
One name and one owner for the pieces, so they travel together and cannot be mixed up by position.
Who owns the <code>text</code> inside a <code>Message</code>?Reveal
The message does. Move the message and the text moves with it; drop it and the text is freed.
Why does <code>record</code> take <code>&amp;Message</code> instead of <code>Message</code>?Reveal
It only needs to look. Borrowing lets the caller keep ownership and use the message afterwards.

That is lesson 5.1. Tick it off so the course remembers where you are.

Learn Rust by building a chat app · Lesson 5.1 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026