Skip to content

Option, Result, and No Null

TypeScript has null, undefined, and exceptions — Rust has none of them

Section titled “TypeScript has null, undefined, and exceptions — Rust has none of them”

TypeScript (and JavaScript) have three ways to represent “something went wrong”:

  1. null — deliberate absence of a value
  2. undefined — an uninitialized or missing value
  3. Thrown exceptions — errors that escape the call stack invisibly

All three are invisible in function signatures. A function that returns string might silently return null, throw an error, or work fine — you cannot tell from the type.

Rust has no null, no undefined, and no exceptions. Absent values are Option<T>. Fallible operations return Result<T, E>. Both are just enum variants, visible in every function signature.

Option<T> is an enum with two variants: Some(T) (there is a value) and None (there is no value). The compiler forces you to handle both.

TypeScript
// TypeScript — null can appear silently
function findUser(id: number): string | null {
if (id === 1) return "Alice";
return null; // easy to forget to check
}
const user = findUser(2);
console.log(user?.toUpperCase() ?? "not found");
// Without optional chaining: user.toUpperCase() would throw TypeError
Rust
fn find_user(id: u32) -> Option<String> {
if id == 1 { Some("Alice".to_string()) } else { None }
}
fn main() {
// Pattern match — must handle both cases
match find_user(2) {
Some(name) => println!("found: {}", name),
None => println!("not found"),
}
// Ergonomic helpers
let upper = find_user(1)
.map(|s| s.to_uppercase())
.unwrap_or_else(|| "NOT FOUND".to_string());
println!("{}", upper);
}

Result<T, E> is an enum with two variants: Ok(T) (success) and Err(E) (failure). Fallible functions return Result, making errors visible in the type signature.

TypeScript
// TypeScript — exceptions are invisible in signatures
function parseNumber(s: string): number {
const n = parseInt(s);
if (isNaN(n)) throw new Error(`not a number: ${s}`);
return n;
}
try {
const n = parseNumber("abc");
console.log(n * 2);
} catch (e) {
// Easy to forget try/catch — and the signature gave no hint
console.error(e);
}
Rust
fn parse_and_double(s: &str) -> Result<i32, String> {
let n: i32 = s.parse().map_err(|_| format!("not a number: {}", s))?;
Ok(n * 2)
}
fn main() {
match parse_and_double("21") {
Ok(v) => println!("doubled: {}", v),
Err(e) => println!("error: {}", e),
}
match parse_and_double("abc") {
Ok(v) => println!("doubled: {}", v),
Err(e) => println!("error: {}", e),
}
}
fn parse_and_double(s: &str) -> Result<i32, String> {
let n: i32 = s.parse().map_err(|_| format!("not a number: {}", s))?;
Ok(n * 2)
}
fn main() {
match parse_and_double("21") {
Ok(v) => println!("doubled: {}", v),
Err(e) => println!("error: {}", e),
}
let maybe: Option<i32> = Some(42);
if let Some(v) = maybe {
println!("got: {}", v);
}
let none: Option<i32> = None;
println!("unwrap_or: {}", none.unwrap_or(0));
}
What does `Option<T>` represent in Rust?
What does the `?` operator do when applied to a `Result<T, E>`?
How is Rust's error model different from TypeScript's `throw`?