Vibe Engines
YouTube

Lesson 5.4 · 9 min

Maybe nothing: Option

What is the argument of "/help"? There is not one. Many languages answer with null and let you find out later. Rust makes "maybe nothing" a type, so you must decide up front.

Adds to Chatterboxfind_command_arg() returns an Option; the bot replies with a Message.

A value that may not be there

A chat command can have an argument: the hello in /say hello. But /help has none. A function that looks for the argument will sometimes come back with nothing.

Rust has no null. Instead, a value that might be missing has its own type, Option<T>, which is just an enum (lesson 5.3) with two variants: Some(value) and None.

The answer is Some(rest) or None, never "empty by accident"
fn find_command_arg(line: &str) -> Option<&str> {
if let Some(i) = line.find(' ') {
Some(&line[i + 1..])
} else {
None
}
}
 
fn main() {
println!("{:?}", find_command_arg("/say hello"));
println!("{:?}", find_command_arg("/help"));
}
$ cargo run
Some("hello")
None

line.find(' ') is itself an Option ({:?} in println! prints a value in debug form, which is how Some("hello") shows; later, matches!(x, Pat) is a yes/no match): the position of the first space, or nothing. if let Some(i) = ... reads "if this is a Some, call what is inside i and run the block; otherwise run the else". It is a match with just the one arm you care about.

The return type Option<&str> is a borrowed slice (lesson 4.5) that may be absent. It points into line, so nothing is copied.

You cannot use it as if it were there

Treating a maybe as a definitely
fn find_command_arg(line: &str) -> Option<&str> {
if let Some(i) = line.find(' ') {
Some(&line[i + 1..])
} else {
None
}
}
 
fn main() {
let arg: &str = find_command_arg("/help");
println!("{}", arg);
}
error[E0308]: mismatched types
  --> src/main.rs:10:21
   |
10 |     let arg: &str = find_command_arg("/help");
   |              ----   ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `Option<&str>`
   |              |
   |              expected due to this
   |
   = note: expected reference `&str`
                   found enum `Option<&str>`
help: consider using `Option::expect` to unwrap the `Option<&str>` value, panicking if the value is an `Option::None`
   |
10 |     let arg: &str = find_command_arg("/help").expect("REASON");
   |                                              +++++++++++++++++

E0308: "mismatched types: expected &str, found Option<&str>". The compiler is saying: this might be nothing, so you cannot treat it as text yet. That is the whole trick. In a language with null, this compiles and crashes when the line was /help. Here the crash is not expressible.

The simplest way out is unwrap_or: "give me the value, or this fallback if there is none".

PredictUse a fallback
fn find_command_arg(line: &str) -> Option<&str> {
if let Some(i) = line.find(' ') {
Some(&line[i + 1..])
} else {
None
}
}
 
fn main() {
let a = find_command_arg("/say hi");
let b = find_command_arg("/help");
println!("{} | {}", a.unwrap_or("none"), b.unwrap_or("none"));
}

What does this print?

  • hi | none
  • hi |
  • it panics
Reveal the answer

The program prints: hi | none a is Some("hi"), so unwrap_or returns hi. b is None, so it returns the fallback. The fallback is decided by you, in the code, and never skipped.

Chatterbox v0.5: parse, handle, a Message out

Chapter 5 comes together. A typed line becomes a Command in parse; find_command_arg pulls out the argument (a bare /say is handled explicitly, and /say with the space is what an argument needs, as in 5.3), and unwrap_or("") says what a bare /say means. handle turns the command into a reply Message. The help text now lists /say, and a reply is a Message from bot instead of an echo: string. matches!(cmd, Command::Quit) in main is a yes/no match.

src/main.rs: Chatterbox v0.5
struct Message {
author: String,
text: String,
}
 
enum Command {
Help,
Quit,
Say(String),
Unknown(String),
}
 
fn find_command_arg(line: &str) -> Option<&str> {
if let Some(i) = line.find(' ') {
Some(&line[i + 1..])
} else {
None
}
}
 
fn parse(line: &str) -> Command {
let line = line.trim();
if line == "/help" {
Command::Help
} else if line == "/quit" {
Command::Quit
} else if line == "/say" || line.starts_with("/say ") {
let text = find_command_arg(line).unwrap_or("");
Command::Say(String::from(text))
} else {
Command::Unknown(String::from(line))
}
}
 
fn handle(cmd: Command) -> Message {
let text = match cmd {
Command::Help => String::from("commands: /help /quit /say <text>"),
Command::Quit => String::from("bye"),
Command::Say(t) => t,
Command::Unknown(l) => format!("unknown: {l}"),
};
Message { author: String::from("bot"), text }
}
 
fn main() {
for line in ["/help", "/say hello", "/say", "/dance", "/quit", "/say never"] {
let cmd = parse(line);
let quit = matches!(cmd, Command::Quit);
let msg = handle(cmd);
println!("{}: {}", msg.author, msg.text);
if quit {
break;
}
}
}
$ cargo run
bot: commands: /help /quit /say <text>
bot: hello
bot: 
bot: unknown: /dance
bot: bye

Run it and watch the last line: after /quit the loop breaks, so /say never is never handled. The program now parses text into a typed value, handles every case the compiler can see, and has no null anywhere. Chapter 6 gives it a memory: the whole conversation in a list.

Try itYour turn: shout the argument

Write shout_arg. If the line has an argument, return it in UPPERCASE. If it has none, return NOTHING TO SHOUT. find_command_arg is given.

Write shout_arg. If the line has an argument, return it in UPPERCASE. If it has none, return NOTHING TO SHOUT. find_command_arg is given.

fn find_command_arg(line: &str) -> Option<&str> {
if let Some(i) = line.find(' ') {
Some(&line[i + 1..])
} else {
None
}
}
 
fn shout_arg(line: &str) -> String {
// use find_command_arg and decide what to do for Some and None
String::new()
}
 
fn main() {
println!("{}", shout_arg("/say hello"));
}

Hidden tests (3): keeps_the_whole_argument nothing_to_shout shouts_the_argument

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 are the two variants of <code>Option&lt;T&gt;</code>?Reveal
Some(value) when there is a value and None when there is not.
Why does Rust refuse to use an <code>Option&lt;&amp;str&gt;</code> where a <code>&amp;str</code> is expected?Reveal
Because the value might be missing. You have to say what happens for None first, with match, if let or unwrap_or, so a missing value can never crash the program by surprise.
What does <code>unwrap_or("none")</code> do?Reveal
Returns the value inside Some, or the fallback you gave if it is None.

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

Next: 6.1 A list: Vec →
Learn Rust by building a chat app · Lesson 5.4 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026