CONCEPT 08 · ABSTRACTIONS
The Monad Pattern
"Chain operations where each step might fail or be async."
Plain English
A monad is a pattern for chaining operations where each step might change the "context" — it might fail, be asynchronous, produce multiple results, etc. The key behavior: if any step in the chain fails or produces None, the remaining steps are skipped automatically. You write each step independently; the pattern handles the routing.
Analogy
A conveyor belt with an automatic ejection system. Each station does one job. If any station detects a problem, the item is ejected and bypasses all remaining stations. You write each station's logic independently — you don't add "if previous station failed, skip me" to every station. The belt handles that.
You already know this
JavaScript Promises: .then() is flatMap. Rejection short-circuits the chain to .catch()async/await: just syntactic sugar for Promise chaining — same monad, nicer syntaxOptional chaining: user?.address?.city — returns undefined on first null, skips the restArray.flatMap(): maps and flattens — flatMap is the core monadic operationGo error handling pattern: if err != nil { return err } repeated — this is what monads automate
Interactive Demo
Option/Maybe chain — click any step to return None
This is how Promises, async/await, and optional chaining work under the hood. One failure = the rest skip automatically.
getUser(42)
Some(User{id:42, name:"Alice"})
→
getOrders(user)
Some([Order#1, Order#2])
→
calcTotal(orders)
Some(Total: $148.00)
Final: Some — Total: $148.00
Code Example
JavaScript
// WITHOUT monadic chaining — manual null/error checks at every step
async function getUserInvoice(userId) {
const user = await fetchUser(userId);
if (!user) return null; // manual check
const orders = await fetchOrders(user.id);
if (!orders || orders.length === 0) return null; // manual check
const invoice = await generateInvoice(orders);
if (!invoice) return null; // manual check
return invoice;
}
// WITH monadic chaining (Promise IS a monad)
// .then() only runs if the previous step succeeded
// .catch() handles ANY failure in the entire chain
async function getUserInvoice(userId) {
return fetchUser(userId)
.then(user => fetchOrders(user.id)) // skipped if user null
.then(orders => generateInvoice(orders)) // skipped if orders null
.catch(err => ({ error: err.message })); // handles any step's failure
}
// Result/Option monad — same pattern, explicit types
function findUser(id) { return id > 0 ? Some(users[id]) : None; }
function findAddress(u) { return u.addressId ? Some(addrs[u.addressId]) : None; }
function formatCity(a) { return Some(a.city.toUpperCase()); }
// flatMap chains — None from any step skips the rest
const city = findUser(42)
.flatMap(user => findAddress(user))
.flatMap(address => formatCity(address));
// city is Some("NEW YORK") or None — never a crashApply when
▸Chaining operations that can fail: fetchUser → fetchProfile → fetchPosts — each step can fail
▸Sequences of async operations where each step depends on the previous
▸Multi-step validation where you want to stop at the first error (fail-fast)
▸Database transactions: a series of writes where any failure should abort the whole sequence
▸Any time you write "if (result != null && result.success) { ... next step ... }" — that's a monad waiting to happen
Check Your Understanding
You have: fetchUser(id).then(u => fetchProfile(u.id)).then(p => fetchPosts(p.blogId)). What happens if fetchUser resolves with null (user not found)?