CONCEPT 03 · FOUNDATIONS
Errors as Values
"Return failures — don't throw them."
Plain English
Instead of throwing exceptions, return a value that explicitly represents either success or failure. The caller receives both possibilities and must handle them — they can't accidentally ignore an error. This makes error handling visible in the code structure, composable with other operations, and impossible to skip.
Analogy
An ATM doesn't throw an exception and crash when your balance is too low. It returns a result: "approved" or "insufficient funds." The machine shows you the outcome and lets you decide what to do. Errors as values work the same way — failure is just another return value, not a system disruption.
You already know this
Go: func getUser() (User, error) — always returns both; caller must check errorRust: Result<T, E> — the type system forces you to handle both casesJavaScript fetch(): returns a rejected Promise — you handle it in .catch()Optional chaining: user?.address?.city — returns undefined on failure, not a crash
Interactive Demo
Click any step to toggle success ✓ / failure ✗
A failure at any step automatically skips everything after it. You don't write any if/else — the chain handles it.
Validate Email
"user@email.com"
→
Find User in DB
User{id:42}
→
Check Account
Account{active}
→
Process Payment
Receipt{#8821}
Result: Right (Success) — Receipt{#8821}
All steps completed.
All steps completed.
Code Example
JavaScript
// THROWING — caller has no idea this can fail
function parseAge(str) {
const n = parseInt(str);
if (isNaN(n)) throw new Error('Not a number'); // invisible!
return n;
}
// Easy to forget: const age = parseAge(input); // might crash!
// RETURNING ERROR — caller MUST handle both paths
function parseAge(str) {
const n = parseInt(str);
if (isNaN(n)) return { ok: false, error: 'Not a number' };
if (n < 0 || n > 150) return { ok: false, error: 'Out of range' };
return { ok: true, value: n };
}
const result = parseAge(userInput);
if (result.ok) {
console.log('Age is:', result.value);
} else {
showError(result.error); // forced to handle it
}
// CHAINING errors as values (Railway-style)
const pipeline =
parseAge(input) // {ok, value/error}
.flatMap(age => validateRange(age)) // only runs if ok
.flatMap(age => saveToProfile(age)); // only runs if ok
// If any step fails, the rest are skipped automaticallyApply when
▸Parsing user input (JSON, dates, numbers) — parsing can legitimately fail, make that visible
▸Network/database calls — anything that talks to the outside world can fail
▸File I/O — file might not exist, permissions might be wrong
▸When you're writing try/catch everywhere: that's a sign errors should be values instead
▸API boundaries — callers of your library should see possible failures in the return type
Check Your Understanding
Your function reads a config file. If the file doesn't exist, what's the best approach?