🚨 Lesson 7.2: Custom Errors and Exceptions
In the last lesson, you learned to catch errors. Now it's time to create them. When a user tries to withdraw more money than they have, or registers with a username that's already taken, a generic Error doesn't tell anyone what actually went wrong. Custom exceptions give your code a vocabulary for communicating specific problems — making bugs easier to find and error handling more precise.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Raise/throw built-in exceptions with meaningful messages
- Create custom exception classes that extend built-in error types
- Add extra data to your custom exceptions (error codes, context)
- Build an exception hierarchy for a domain (e.g., banking errors)
- Use guard clauses to validate inputs at function boundaries
- Know when to throw vs. when to return error values
Estimated Time: 50 minutes
Project: Build a bank account system with a custom error hierarchy
📑 In This Lesson
Throwing/Raising Errors
You already know that runtime errors happen automatically — divide by zero, access a missing key, etc. But you can also create and throw errors yourself when your code detects a problem that the language wouldn't catch on its own.
# Python uses 'raise' to throw errors
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
if age > 150:
raise ValueError("Age seems unrealistic")
return age
# This works fine
print(set_age(25)) # 25
# This raises an error you created
try:
set_age(-5)
except ValueError as e:
print(f"Error: {e}") # Error: Age cannot be negative
# You can raise any built-in exception type
raise TypeError("Expected a string, got int")
raise FileNotFoundError("Config file missing: app.yaml")
raise PermissionError("User lacks admin access")
raise RuntimeError("Something unexpected happened")
// JavaScript uses 'throw' to throw errors
function setAge(age) {
if (age < 0) {
throw new RangeError("Age cannot be negative");
}
if (age > 150) {
throw new RangeError("Age seems unrealistic");
}
return age;
}
// This works fine
console.log(setAge(25)); // 25
// This throws an error you created
try {
setAge(-5);
} catch (error) {
console.log(`Error: ${error.message}`); // Error: Age cannot be negative
}
// You can throw any built-in error type
throw new TypeError("Expected a string, got number");
throw new RangeError("Value out of acceptable range");
throw new ReferenceError("Configuration not initialized");
throw new Error("Something unexpected happened");
// JS can technically throw anything — but don't do this!
// throw "bad!"; // ❌ No stack trace, no .message
// throw 42; // ❌ Loses all error information
// throw { msg: "!" }; // ❌ Not an Error instance
// C# uses 'throw' to throw errors
static int SetAge(int age)
{
if (age < 0)
throw new ArgumentOutOfRangeException(nameof(age), "Age cannot be negative");
if (age > 150)
throw new ArgumentOutOfRangeException(nameof(age), "Age seems unrealistic");
return age;
}
// This works fine
Console.WriteLine(SetAge(25)); // 25
// This throws an error you created
try
{
SetAge(-5);
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine($"Error: {e.Message}");
}
// Common built-in exception types to throw
throw new ArgumentNullException(nameof(name), "Name is required");
throw new ArgumentException("Email format is invalid");
throw new InvalidOperationException("Cannot edit a locked record");
throw new NotImplementedException("Feature coming soon");
throw new NotSupportedException("PDF export not available");
Throw/Raise Syntax
| Action | 🐍 Python | ⚡ JavaScript | 🔷 C# |
|---|---|---|---|
| Throw an error | raise ValueError("msg") |
throw new Error("msg") |
throw new Exception("msg") |
| Keyword | raise |
throw |
throw |
Needs new? |
No | Yes | Yes |
| Re-throw current error | raise (bare) |
throw (bare) or throw error |
throw; (bare) |
⚠️ Python Says "raise," Not "throw"
Just like Python uses except instead of catch, it uses raise instead of throw. Python also doesn't need the new keyword — you just call the exception class directly: raise ValueError("msg"). JavaScript and C# both use throw new.
🎓 Instructor Note: Delivery Guidance
Emphasize that throwing errors is about communication — you're telling future developers (including yourself) exactly what went wrong and why. Show the JavaScript "don't throw primitives" warning as a practical tip. In JS, throw "bad!" technically works but you lose the stack trace and .message property, making debugging much harder. Always throw Error instances. The re-throw syntax (raise / throw / throw; with no argument) is important — it preserves the original stack trace instead of creating a new one.
When to Throw
Not every problem deserves an exception. Here's a practical guide:
| Situation | Throw an Error? | Why |
|---|---|---|
| Function receives invalid arguments | Yes | The caller made a mistake — tell them immediately |
| Required resource is missing (file, API, DB) | Yes | Can't continue without it — caller needs to know |
| Business rule violated (overdraft, expired coupon) | Yes | Operation shouldn't proceed — protect data integrity |
| Object is in wrong state for operation | Yes | E.g., editing after "save," connecting already-connected |
| Search returns no results | No | Empty results are normal — return an empty list |
| User input might be invalid | Depends | Validate first; only throw if validation is bypassed |
💡 The Rule of Thumb
Throw when the caller broke a contract — they passed bad data, called things in the wrong order, or a required external resource failed. Don't throw for situations that are a normal part of program flow, like a search returning zero results or a user choosing to cancel.
Custom Exception Classes
Built-in errors like ValueError or TypeError are generic. When your application has specific problems — like "insufficient funds" or "username taken" — you should create custom exceptions that describe exactly what went wrong.
# Custom exceptions inherit from Exception (or a subclass)
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the account balance."""
pass
class AccountLockedError(Exception):
"""Raised when an operation is attempted on a locked account."""
pass
class InvalidAmountError(ValueError):
"""Raised when a monetary amount is invalid (negative or zero)."""
pass
# Using custom exceptions
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.locked = False
def withdraw(self, amount):
if self.locked:
raise AccountLockedError(f"Account for {self.owner} is locked")
if amount <= 0:
raise InvalidAmountError(f"Amount must be positive, got {amount}")
if amount > self.balance:
raise InsufficientFundsError(
f"Cannot withdraw ${amount:.2f} — balance is ${self.balance:.2f}"
)
self.balance -= amount
return self.balance
# Catching custom exceptions
account = BankAccount("Alice", 100)
try:
account.withdraw(150)
except InsufficientFundsError as e:
print(f"💰 {e}")
# 💰 Cannot withdraw $150.00 — balance is $100.00
try:
account.withdraw(-50)
except InvalidAmountError as e:
print(f"❌ {e}")
# ❌ Amount must be positive, got -50
// Custom exceptions extend Error
class InsufficientFundsError extends Error {
constructor(message) {
super(message);
this.name = "InsufficientFundsError"; // Override the name!
}
}
class AccountLockedError extends Error {
constructor(message) {
super(message);
this.name = "AccountLockedError";
}
}
class InvalidAmountError extends RangeError {
constructor(message) {
super(message);
this.name = "InvalidAmountError";
}
}
// Using custom exceptions
class BankAccount {
constructor(owner, balance = 0) {
this.owner = owner;
this.balance = balance;
this.locked = false;
}
withdraw(amount) {
if (this.locked) {
throw new AccountLockedError(`Account for ${this.owner} is locked`);
}
if (amount <= 0) {
throw new InvalidAmountError(`Amount must be positive, got ${amount}`);
}
if (amount > this.balance) {
throw new InsufficientFundsError(
`Cannot withdraw $${amount.toFixed(2)} — balance is $${this.balance.toFixed(2)}`
);
}
this.balance -= amount;
return this.balance;
}
}
// Catching custom exceptions
let account = new BankAccount("Alice", 100);
try {
account.withdraw(150);
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.log(`💰 ${error.message}`);
}
}
// 💰 Cannot withdraw $150.00 — balance is $100.00
// Custom exceptions inherit from Exception (or a subclass)
class InsufficientFundsException : Exception
{
public InsufficientFundsException(string message) : base(message) { }
}
class AccountLockedException : InvalidOperationException
{
public AccountLockedException(string message) : base(message) { }
}
class InvalidAmountException : ArgumentException
{
public InvalidAmountException(string message) : base(message) { }
}
// Using custom exceptions
class BankAccount
{
public string Owner;
public decimal Balance;
public bool Locked;
public BankAccount(string owner, decimal balance = 0)
{
Owner = owner;
Balance = balance;
Locked = false;
}
public decimal Withdraw(decimal amount)
{
if (Locked)
throw new AccountLockedException($"Account for {Owner} is locked");
if (amount <= 0)
throw new InvalidAmountException($"Amount must be positive, got {amount}");
if (amount > Balance)
throw new InsufficientFundsException(
$"Cannot withdraw {amount:C} — balance is {Balance:C}");
Balance -= amount;
return Balance;
}
}
// Catching custom exceptions
var account = new BankAccount("Alice", 100);
try
{
account.Withdraw(150);
}
catch (InsufficientFundsException e)
{
Console.WriteLine($"💰 {e.Message}");
}
// 💰 Cannot withdraw $150.00 — balance is $100.00
Custom Exception Syntax
| Feature | 🐍 Python | ⚡ JavaScript | 🔷 C# |
|---|---|---|---|
| Base class | Exception |
Error |
Exception |
| Minimal definition | class MyError(Exception): pass |
Constructor + this.name |
Constructor calling : base(msg) |
| Naming convention | SomethingError |
SomethingError |
SomethingException |
⚠️ JavaScript: Always Set this.name
If you don't set this.name in your custom JS error class, the error will display as "Error" in logs and stack traces instead of "InsufficientFundsError". Python and C# get the class name automatically — JavaScript requires you to set it manually.
💡 C# Convention: "Exception" Suffix
C# uses the suffix Exception (e.g., InsufficientFundsException) while Python and JavaScript use Error (e.g., InsufficientFundsError). This is just a naming convention — follow whichever your language community expects.
🎓 Instructor Note: Delivery Guidance
The bank account is a perfect teaching domain because every student understands withdrawals, deposits, and balances. Walk through the withdraw() method line by line: "First we check if the account is locked — different error. Then we check if the amount is valid — different error. Then we check if there's enough money — different error." Each check uses a different custom exception, which means the caller can handle each situation differently. Compare this to a single generic raise Exception("something went wrong") — the caller would have no idea what happened. Python's minimal syntax (class MyError(Exception): pass) is worth highlighting; it's the simplest custom exception definition across all three languages.
Adding Data to Exceptions
A message string is helpful, but sometimes the caller needs structured data to handle the error properly — like the amount that was short, the account balance, or a specific error code. Custom exceptions can carry any data you want.
class InsufficientFundsError(Exception):
"""Carries the shortage amount so callers can act on it."""
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
self.shortage = amount - balance
super().__init__(
f"Cannot withdraw ${amount:.2f} from ${balance:.2f} "
f"(short ${self.shortage:.2f})"
)
class ValidationError(Exception):
"""Carries a field name and error code."""
def __init__(self, field, message, code=None):
self.field = field
self.code = code
super().__init__(f"{field}: {message}")
# Using the rich exception data
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(balance, amount)
return balance - amount
try:
withdraw(75.00, 100.00)
except InsufficientFundsError as e:
print(f"Error: {e}")
print(f" Balance: ${e.balance:.2f}")
print(f" Requested: ${e.amount:.2f}")
print(f" Short by: ${e.shortage:.2f}")
# Now you can offer: "Would you like to withdraw $75.00 instead?"
# Error: Cannot withdraw $100.00 from $75.00 (short $25.00)
# Balance: $75.00
# Requested: $100.00
# Short by: $25.00
# Validation with error codes
try:
raise ValidationError("email", "Invalid format", code="INVALID_EMAIL")
except ValidationError as e:
print(f"Field: {e.field}, Code: {e.code}, Message: {e}")
class InsufficientFundsError extends Error {
constructor(balance, amount) {
let shortage = amount - balance;
super(
`Cannot withdraw $${amount.toFixed(2)} from $${balance.toFixed(2)} ` +
`(short $${shortage.toFixed(2)})`
);
this.name = "InsufficientFundsError";
this.balance = balance;
this.amount = amount;
this.shortage = shortage;
}
}
class ValidationError extends Error {
constructor(field, message, code = null) {
super(`${field}: ${message}`);
this.name = "ValidationError";
this.field = field;
this.code = code;
}
}
// Using the rich exception data
function withdraw(balance, amount) {
if (amount > balance) {
throw new InsufficientFundsError(balance, amount);
}
return balance - amount;
}
try {
withdraw(75.00, 100.00);
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.log(`Error: ${error.message}`);
console.log(` Balance: $${error.balance.toFixed(2)}`);
console.log(` Requested: $${error.amount.toFixed(2)}`);
console.log(` Short by: $${error.shortage.toFixed(2)}`);
}
}
// Validation with error codes
try {
throw new ValidationError("email", "Invalid format", "INVALID_EMAIL");
} catch (error) {
console.log(`Field: ${error.field}, Code: ${error.code}`);
}
class InsufficientFundsException : Exception
{
public decimal Balance { get; }
public decimal Amount { get; }
public decimal Shortage => Amount - Balance;
public InsufficientFundsException(decimal balance, decimal amount)
: base($"Cannot withdraw {amount:C} from {balance:C} (short {amount - balance:C})")
{
Balance = balance;
Amount = amount;
}
}
class ValidationException : Exception
{
public string Field { get; }
public string? Code { get; }
public ValidationException(string field, string message, string? code = null)
: base($"{field}: {message}")
{
Field = field;
Code = code;
}
}
// Using the rich exception data
static decimal Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
throw new InsufficientFundsException(balance, amount);
return balance - amount;
}
try
{
Withdraw(75.00m, 100.00m);
}
catch (InsufficientFundsException e)
{
Console.WriteLine($"Error: {e.Message}");
Console.WriteLine($" Balance: {e.Balance:C}");
Console.WriteLine($" Requested: {e.Amount:C}");
Console.WriteLine($" Short by: {e.Shortage:C}");
}
// Validation with error codes
try
{
throw new ValidationException("email", "Invalid format", "INVALID_EMAIL");
}
catch (ValidationException e)
{
Console.WriteLine($"Field: {e.Field}, Code: {e.Code}");
}
✅ Why Data-Rich Exceptions Matter
With extra data on the exception, the caller can make smart decisions. Instead of just showing "insufficient funds," the UI could say: "You're $25.00 short. Would you like to withdraw $75.00 instead?" The error code (INVALID_EMAIL) lets the frontend highlight the right field and show the right message — without parsing error strings.
Exception Hierarchies
Just as classes can form inheritance trees, exceptions can too. A hierarchy lets callers choose how specifically they want to handle errors — they can catch the exact type or a broader parent type.
# Base error for all banking operations
class BankError(Exception):
"""Base class for all banking exceptions."""
pass
# Account-related errors
class AccountError(BankError):
"""Base for account-level problems."""
def __init__(self, account_id, message):
self.account_id = account_id
super().__init__(f"Account {account_id}: {message}")
class AccountLockedError(AccountError):
def __init__(self, account_id, reason="policy violation"):
self.reason = reason
super().__init__(account_id, f"Locked ({reason})")
class AccountNotFoundError(AccountError):
def __init__(self, account_id):
super().__init__(account_id, "Not found")
# Transaction-related errors
class TransactionError(BankError):
"""Base for transaction-level problems."""
def __init__(self, amount, message):
self.amount = amount
super().__init__(f"Transaction of ${amount:.2f}: {message}")
class InsufficientFundsError(TransactionError):
def __init__(self, amount, balance):
self.balance = balance
self.shortage = amount - balance
super().__init__(amount, f"Insufficient funds (balance: ${balance:.2f})")
class DailyLimitExceededError(TransactionError):
def __init__(self, amount, limit):
self.limit = limit
super().__init__(amount, f"Exceeds daily limit of ${limit:.2f}")
# --- Catching at different levels of specificity ---
# Specific: handle just insufficient funds
try:
raise InsufficientFundsError(500, 200)
except InsufficientFundsError as e:
print(f"Short ${e.shortage:.2f}")
# Medium: handle any transaction error
try:
raise DailyLimitExceededError(5000, 2000)
except TransactionError as e:
print(f"Transaction failed: {e}")
# Broad: handle any banking error
try:
raise AccountLockedError("ACC-123", "fraud investigation")
except BankError as e:
print(f"Banking error: {e}")
// Base error for all banking operations
class BankError extends Error {
constructor(message) {
super(message);
this.name = "BankError";
}
}
// Account-related errors
class AccountError extends BankError {
constructor(accountId, message) {
super(`Account ${accountId}: ${message}`);
this.name = "AccountError";
this.accountId = accountId;
}
}
class AccountLockedError extends AccountError {
constructor(accountId, reason = "policy violation") {
super(accountId, `Locked (${reason})`);
this.name = "AccountLockedError";
this.reason = reason;
}
}
class AccountNotFoundError extends AccountError {
constructor(accountId) {
super(accountId, "Not found");
this.name = "AccountNotFoundError";
}
}
// Transaction-related errors
class TransactionError extends BankError {
constructor(amount, message) {
super(`Transaction of $${amount.toFixed(2)}: ${message}`);
this.name = "TransactionError";
this.amount = amount;
}
}
class InsufficientFundsError extends TransactionError {
constructor(amount, balance) {
super(amount, `Insufficient funds (balance: $${balance.toFixed(2)})`);
this.name = "InsufficientFundsError";
this.balance = balance;
this.shortage = amount - balance;
}
}
// --- Catching at different levels ---
try {
throw new InsufficientFundsError(500, 200);
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.log(`Short $${error.shortage.toFixed(2)}`);
} else if (error instanceof TransactionError) {
console.log(`Transaction failed: ${error.message}`);
} else if (error instanceof BankError) {
console.log(`Banking error: ${error.message}`);
}
}
// instanceof checks the ENTIRE chain
let err = new InsufficientFundsError(100, 50);
console.log(err instanceof InsufficientFundsError); // true
console.log(err instanceof TransactionError); // true
console.log(err instanceof BankError); // true
console.log(err instanceof Error); // true
// Base error for all banking operations
class BankException : Exception
{
public BankException(string message) : base(message) { }
}
// Account-related errors
class AccountException : BankException
{
public string AccountId { get; }
public AccountException(string accountId, string message)
: base($"Account {accountId}: {message}")
{ AccountId = accountId; }
}
class AccountLockedException : AccountException
{
public string Reason { get; }
public AccountLockedException(string accountId, string reason = "policy violation")
: base(accountId, $"Locked ({reason})")
{ Reason = reason; }
}
class AccountNotFoundException : AccountException
{
public AccountNotFoundException(string accountId)
: base(accountId, "Not found") { }
}
// Transaction-related errors
class TransactionException : BankException
{
public decimal Amount { get; }
public TransactionException(decimal amount, string message)
: base($"Transaction of {amount:C}: {message}")
{ Amount = amount; }
}
class InsufficientFundsException : TransactionException
{
public decimal Balance { get; }
public decimal Shortage => Amount - Balance;
public InsufficientFundsException(decimal amount, decimal balance)
: base(amount, $"Insufficient funds (balance: {balance:C})")
{ Balance = balance; }
}
// --- Catching at different levels ---
// Specific
try { throw new InsufficientFundsException(500, 200); }
catch (InsufficientFundsException e)
{ Console.WriteLine($"Short {e.Shortage:C}"); }
// Medium
try { throw new InsufficientFundsException(500, 200); }
catch (TransactionException e)
{ Console.WriteLine($"Transaction failed: {e.Message}"); }
// Broad
try { throw new AccountLockedException("ACC-123", "fraud"); }
catch (BankException e)
{ Console.WriteLine($"Banking error: {e.Message}"); }
🎓 Instructor Note: Delivery Guidance
The Mermaid diagram is key here — let students see the tree structure before diving into code. Walk through the catching levels: "If I catch TransactionError, that catches both InsufficientFundsError and DailyLimitExceededError. If I catch BankError, I catch everything." This is exactly the same polymorphism they learned in Lesson 18 — exceptions are just classes, and isinstance/instanceof/is works the same way. Note that JavaScript's single catch block means you use instanceof chains instead of multiple catch blocks — less elegant but equally functional.
Guard Clauses
A guard clause is a check at the top of a function that immediately throws if the input is invalid. Guards make your code cleaner — they eliminate deeply nested if/else blocks by handling the "bad" cases first and letting the "happy path" flow naturally.
# ❌ Without guards — deeply nested "pyramid of doom"
def create_user(username, email, age):
if username:
if len(username) >= 3:
if email:
if "@" in email:
if age:
if age >= 13:
# FINALLY, the actual logic, buried 6 levels deep
return {"username": username, "email": email, "age": age}
else:
raise ValueError("Must be 13+")
else:
raise ValueError("Age required")
else:
raise ValueError("Invalid email")
else:
raise ValueError("Email required")
else:
raise ValueError("Username too short")
else:
raise ValueError("Username required")
# ✅ With guards — flat, clear, and readable
def create_user(username, email, age):
# Guards: check each requirement, bail immediately if invalid
if not username:
raise ValueError("Username is required")
if len(username) < 3:
raise ValueError("Username must be at least 3 characters")
if not email:
raise ValueError("Email is required")
if "@" not in email:
raise ValueError("Email must contain @")
if not age:
raise ValueError("Age is required")
if age < 13:
raise ValueError("Must be at least 13 years old")
# Happy path — no nesting needed
return {"username": username, "email": email, "age": age}
// ❌ Without guards — deeply nested
function createUser(username, email, age) {
if (username) {
if (username.length >= 3) {
if (email) {
if (email.includes("@")) {
if (age) {
if (age >= 13) {
return { username, email, age };
} else { throw new RangeError("Must be 13+"); }
} else { throw new Error("Age required"); }
} else { throw new Error("Invalid email"); }
} else { throw new Error("Email required"); }
} else { throw new Error("Username too short"); }
} else { throw new Error("Username required"); }
}
// ✅ With guards — flat, clear, and readable
function createUser(username, email, age) {
if (!username) throw new Error("Username is required");
if (username.length < 3) throw new Error("Username must be at least 3 characters");
if (!email) throw new Error("Email is required");
if (!email.includes("@")) throw new Error("Email must contain @");
if (!age) throw new Error("Age is required");
if (age < 13) throw new RangeError("Must be at least 13 years old");
// Happy path — no nesting
return { username, email, age };
}
// ✅ With guards — clean and modern C#
static User CreateUser(string? username, string? email, int? age)
{
if (string.IsNullOrWhiteSpace(username))
throw new ArgumentException("Username is required");
if (username.Length < 3)
throw new ArgumentException("Username must be at least 3 characters");
if (string.IsNullOrWhiteSpace(email))
throw new ArgumentException("Email is required");
if (!email.Contains('@'))
throw new ArgumentException("Email must contain @");
if (age is null)
throw new ArgumentNullException(nameof(age), "Age is required");
if (age < 13)
throw new ArgumentOutOfRangeException(nameof(age), "Must be at least 13");
// Happy path
return new User { Username = username, Email = email, Age = age.Value };
}
// C# 11+ also has ArgumentException.ThrowIfNullOrEmpty()
static void QuickGuards(string name, int count)
{
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentOutOfRangeException.ThrowIfNegative(count);
// ... happy path
}
✅ Guard Clause Benefits
- Flat code — no nesting pyramid, easy to scan
- Fail fast — invalid inputs are rejected immediately
- Happy path is obvious — it's all the code after the guards
- Each guard is independent — easy to add, remove, or reorder validations
Throw vs. Return
When something goes wrong, should you throw an error or return a special value (like null or an error object)? Both have their place.
| Approach | Best For | Pros | Cons |
|---|---|---|---|
| Throw | Broken contracts, unrecoverable states | Can't be ignored, clear stack trace | Interrupts flow, costs performance if frequent |
| Return null/None | "Not found" results | Simple, expected by callers | Easy to forget to check, null propagation bugs |
| Return result object | Operations that may or may not succeed | Forces caller to handle both paths | More code to write and consume |
# Pattern 1: Throw — for contract violations
def divide(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
return a / b
# Pattern 2: Return None — for "not found" searches
def find_user(users, name):
for user in users:
if user["name"] == name:
return user
return None # Not found is a normal outcome
# Pattern 3: Return a result tuple/dict — forces handling
def validate_password(password):
errors = []
if len(password) < 8:
errors.append("Must be at least 8 characters")
if not any(c.isupper() for c in password):
errors.append("Must contain an uppercase letter")
if not any(c.isdigit() for c in password):
errors.append("Must contain a digit")
if errors:
return {"valid": False, "errors": errors}
return {"valid": True, "errors": []}
# Usage:
result = validate_password("abc")
if not result["valid"]:
for error in result["errors"]:
print(f" ❌ {error}")
# ❌ Must be at least 8 characters
# ❌ Must contain an uppercase letter
# ❌ Must contain a digit
// Pattern 1: Throw — for contract violations
function divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
}
// Pattern 2: Return null — for "not found" searches
function findUser(users, name) {
return users.find(u => u.name === name) || null;
}
// Pattern 3: Return a result object — forces handling
function validatePassword(password) {
let errors = [];
if (password.length < 8)
errors.push("Must be at least 8 characters");
if (!/[A-Z]/.test(password))
errors.push("Must contain an uppercase letter");
if (!/\d/.test(password))
errors.push("Must contain a digit");
return { valid: errors.length === 0, errors };
}
let result = validatePassword("abc");
if (!result.valid) {
result.errors.forEach(err => console.log(` ❌ ${err}`));
}
// Pattern 1: Throw — for contract violations
static double Divide(double a, double b)
{
if (b == 0) throw new DivideByZeroException("Cannot divide by zero");
return a / b;
}
// Pattern 2: TryParse pattern — caller decides how to handle
// This is a very common C# idiom
static bool TryFindUser(List<User> users, string name, out User? found)
{
found = users.FirstOrDefault(u => u.Name == name);
return found != null;
}
// Usage:
if (TryFindUser(users, "Alice", out var user))
Console.WriteLine($"Found: {user.Name}");
else
Console.WriteLine("User not found");
// Pattern 3: Return a result object
record ValidationResult(bool Valid, List<string> Errors);
static ValidationResult ValidatePassword(string password)
{
var errors = new List<string>();
if (password.Length < 8)
errors.Add("Must be at least 8 characters");
if (!password.Any(char.IsUpper))
errors.Add("Must contain an uppercase letter");
if (!password.Any(char.IsDigit))
errors.Add("Must contain a digit");
return new ValidationResult(errors.Count == 0, errors);
}
var result = ValidatePassword("abc");
if (!result.Valid)
result.Errors.ForEach(e => Console.WriteLine($" ❌ {e}"));
💡 C#'s TryParse Pattern
C# has a beloved convention: methods that might fail return a bool and put the result in an out parameter. You see this everywhere: int.TryParse(), Dictionary.TryGetValue(), etc. It's the "return" approach baked into the language's DNA.
Exercises
🏋️ Exercise 1: User Registration System
Objective: Build a registration system with custom exceptions:
- Create custom exceptions:
RegistrationError(base),UsernameTakenError,WeakPasswordError,InvalidEmailError WeakPasswordErrorshould list the specific requirements that failed- Build a
register_user()function that validates and either succeeds or throws the appropriate custom exception - Maintain a list of existing users to check for duplicate usernames
- Demonstrate catching at different levels: specific error types and the base
RegistrationError
✅ Solution
class RegistrationError(Exception):
"""Base for all registration problems."""
pass
class UsernameTakenError(RegistrationError):
def __init__(self, username):
self.username = username
super().__init__(f"Username '{username}' is already taken")
class WeakPasswordError(RegistrationError):
def __init__(self, failures):
self.failures = failures
msg = "Password too weak: " + "; ".join(failures)
super().__init__(msg)
class InvalidEmailError(RegistrationError):
def __init__(self, email):
self.email = email
super().__init__(f"Invalid email address: '{email}'")
# Existing users database (simulated)
existing_users = ["alice", "bob", "charlie"]
def register_user(username, email, password):
# Guard clauses with custom exceptions
if not username or len(username) < 3:
raise RegistrationError("Username must be at least 3 characters")
if username.lower() in existing_users:
raise UsernameTakenError(username)
if not email or "@" not in email or "." not in email.split("@")[-1]:
raise InvalidEmailError(email)
# Password validation
pw_failures = []
if len(password) < 8:
pw_failures.append("at least 8 characters")
if not any(c.isupper() for c in password):
pw_failures.append("one uppercase letter")
if not any(c.islower() for c in password):
pw_failures.append("one lowercase letter")
if not any(c.isdigit() for c in password):
pw_failures.append("one digit")
if pw_failures:
raise WeakPasswordError(pw_failures)
# Success!
existing_users.append(username.lower())
return {"username": username, "email": email, "status": "active"}
# --- Test specific catches ---
test_cases = [
("alice", "alice@test.com", "StrongPass1"), # Username taken
("newuser", "bad-email", "StrongPass1"), # Invalid email
("newuser", "new@test.com", "weak"), # Weak password
("newuser", "new@test.com", "StrongPass1"), # Success!
]
for username, email, password in test_cases:
try:
user = register_user(username, email, password)
print(f"✅ Registered: {user['username']}")
except UsernameTakenError as e:
print(f"👤 {e}")
except InvalidEmailError as e:
print(f"📧 {e}")
except WeakPasswordError as e:
print(f"🔒 {e}")
print(f" Missing: {', '.join(e.failures)}")
except RegistrationError as e:
print(f"❌ Registration failed: {e}")
class RegistrationError extends Error {
constructor(message) {
super(message);
this.name = "RegistrationError";
}
}
class UsernameTakenError extends RegistrationError {
constructor(username) {
super(`Username '${username}' is already taken`);
this.name = "UsernameTakenError";
this.username = username;
}
}
class WeakPasswordError extends RegistrationError {
constructor(failures) {
super("Password too weak: " + failures.join("; "));
this.name = "WeakPasswordError";
this.failures = failures;
}
}
class InvalidEmailError extends RegistrationError {
constructor(email) {
super(`Invalid email address: '${email}'`);
this.name = "InvalidEmailError";
this.email = email;
}
}
let existingUsers = ["alice", "bob", "charlie"];
function registerUser(username, email, password) {
if (!username || username.length < 3)
throw new RegistrationError("Username must be at least 3 characters");
if (existingUsers.includes(username.toLowerCase()))
throw new UsernameTakenError(username);
if (!email || !email.includes("@") || !email.split("@")[1]?.includes("."))
throw new InvalidEmailError(email);
let failures = [];
if (password.length < 8) failures.push("at least 8 characters");
if (!/[A-Z]/.test(password)) failures.push("one uppercase letter");
if (!/[a-z]/.test(password)) failures.push("one lowercase letter");
if (!/\d/.test(password)) failures.push("one digit");
if (failures.length) throw new WeakPasswordError(failures);
existingUsers.push(username.toLowerCase());
return { username, email, status: "active" };
}
// Test
let tests = [
["alice", "alice@test.com", "StrongPass1"],
["newuser", "bad-email", "StrongPass1"],
["newuser", "new@test.com", "weak"],
["newuser", "new@test.com", "StrongPass1"],
];
for (let [username, email, password] of tests) {
try {
let user = registerUser(username, email, password);
console.log(`✅ Registered: ${user.username}`);
} catch (error) {
if (error instanceof UsernameTakenError) {
console.log(`👤 ${error.message}`);
} else if (error instanceof InvalidEmailError) {
console.log(`📧 ${error.message}`);
} else if (error instanceof WeakPasswordError) {
console.log(`🔒 ${error.message}`);
} else if (error instanceof RegistrationError) {
console.log(`❌ ${error.message}`);
}
}
}
class RegistrationException : Exception
{
public RegistrationException(string msg) : base(msg) { }
}
class UsernameTakenException : RegistrationException
{
public string Username { get; }
public UsernameTakenException(string username)
: base($"Username '{username}' is already taken")
{ Username = username; }
}
class WeakPasswordException : RegistrationException
{
public List<string> Failures { get; }
public WeakPasswordException(List<string> failures)
: base("Password too weak: " + string.Join("; ", failures))
{ Failures = failures; }
}
class InvalidEmailException : RegistrationException
{
public string Email { get; }
public InvalidEmailException(string email)
: base($"Invalid email: '{email}'")
{ Email = email; }
}
List<string> existingUsers = new() { "alice", "bob", "charlie" };
Dictionary<string, string> RegisterUser(string username, string email, string password)
{
if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
throw new RegistrationException("Username must be at least 3 characters");
if (existingUsers.Contains(username.ToLower()))
throw new UsernameTakenException(username);
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
throw new InvalidEmailException(email);
var failures = new List<string>();
if (password.Length < 8) failures.Add("at least 8 characters");
if (!password.Any(char.IsUpper)) failures.Add("one uppercase letter");
if (!password.Any(char.IsLower)) failures.Add("one lowercase letter");
if (!password.Any(char.IsDigit)) failures.Add("one digit");
if (failures.Count > 0) throw new WeakPasswordException(failures);
existingUsers.Add(username.ToLower());
return new() { ["username"] = username, ["status"] = "active" };
}
try { RegisterUser("alice", "a@b.com", "StrongPass1"); }
catch (UsernameTakenException e) { Console.WriteLine($"👤 {e.Message}"); }
try { RegisterUser("new", "bad", "StrongPass1"); }
catch (InvalidEmailException e) { Console.WriteLine($"📧 {e.Message}"); }
try { RegisterUser("new", "n@b.com", "weak"); }
catch (WeakPasswordException e) { Console.WriteLine($"🔒 {e.Message}"); }
🏋️ Exercise 2: Online Store Order System
Objective: Build an order processing system with a custom error hierarchy:
OrderError(base) →OutOfStockError(withproduct,requested,available) andInvalidQuantityError- A product inventory dictionary with stock levels
- An
order_item(product, quantity)function that validates and processes orders - A
process_cart(items)function that processes multiple items, collecting errors for each failed item instead of stopping at the first error - Display a summary: which items succeeded, which failed, and why
✅ Solution
class OrderError(Exception):
"""Base for all order problems."""
pass
class OutOfStockError(OrderError):
def __init__(self, product, requested, available):
self.product = product
self.requested = requested
self.available = available
super().__init__(
f"'{product}': requested {requested}, only {available} available"
)
class InvalidQuantityError(OrderError):
def __init__(self, product, quantity):
self.product = product
self.quantity = quantity
super().__init__(f"'{product}': invalid quantity {quantity}")
# Inventory
inventory = {
"Widget": {"price": 9.99, "stock": 50},
"Gadget": {"price": 24.99, "stock": 3},
"Gizmo": {"price": 14.99, "stock": 0},
"Doohickey": {"price": 4.99, "stock": 100},
}
def order_item(product, quantity):
if product not in inventory:
raise OrderError(f"Product '{product}' not found")
if quantity <= 0 or not isinstance(quantity, int):
raise InvalidQuantityError(product, quantity)
available = inventory[product]["stock"]
if quantity > available:
raise OutOfStockError(product, quantity, available)
inventory[product]["stock"] -= quantity
cost = inventory[product]["price"] * quantity
return {"product": product, "quantity": quantity, "cost": cost}
def process_cart(items):
successes = []
failures = []
for product, quantity in items:
try:
result = order_item(product, quantity)
successes.append(result)
except OutOfStockError as e:
failures.append({"product": product, "error": str(e), "type": "stock"})
except InvalidQuantityError as e:
failures.append({"product": product, "error": str(e), "type": "quantity"})
except OrderError as e:
failures.append({"product": product, "error": str(e), "type": "general"})
return successes, failures
# Process a cart
cart = [
("Widget", 5), # ✅ Success
("Gizmo", 2), # ❌ Out of stock (0 available)
("Gadget", 10), # ❌ Out of stock (only 3)
("Flux Cap", 1), # ❌ Product not found
("Widget", -3), # ❌ Invalid quantity
("Doohickey", 20), # ✅ Success
]
successes, failures = process_cart(cart)
print("=== Order Summary ===\n")
print("✅ Successful:")
total = 0
for item in successes:
print(f" {item['product']} x{item['quantity']} = ${item['cost']:.2f}")
total += item["cost"]
print(f" Total: ${total:.2f}\n")
print("❌ Failed:")
for fail in failures:
print(f" {fail['error']}")
class OrderError extends Error {
constructor(message) { super(message); this.name = "OrderError"; }
}
class OutOfStockError extends OrderError {
constructor(product, requested, available) {
super(`'${product}': requested ${requested}, only ${available} available`);
this.name = "OutOfStockError";
this.product = product;
this.requested = requested;
this.available = available;
}
}
class InvalidQuantityError extends OrderError {
constructor(product, quantity) {
super(`'${product}': invalid quantity ${quantity}`);
this.name = "InvalidQuantityError";
this.product = product;
this.quantity = quantity;
}
}
let inventory = {
Widget: { price: 9.99, stock: 50 },
Gadget: { price: 24.99, stock: 3 },
Gizmo: { price: 14.99, stock: 0 },
Doohickey: { price: 4.99, stock: 100 },
};
function orderItem(product, quantity) {
if (!inventory[product]) throw new OrderError(`Product '${product}' not found`);
if (quantity <= 0 || !Number.isInteger(quantity))
throw new InvalidQuantityError(product, quantity);
if (quantity > inventory[product].stock)
throw new OutOfStockError(product, quantity, inventory[product].stock);
inventory[product].stock -= quantity;
return { product, quantity, cost: inventory[product].price * quantity };
}
function processCart(items) {
let successes = [], failures = [];
for (let [product, qty] of items) {
try {
successes.push(orderItem(product, qty));
} catch (error) {
failures.push({ product, error: error.message, type: error.name });
}
}
return { successes, failures };
}
let cart = [["Widget", 5], ["Gizmo", 2], ["Gadget", 10],
["Flux Cap", 1], ["Widget", -3], ["Doohickey", 20]];
let { successes, failures } = processCart(cart);
console.log("✅ Successful:");
successes.forEach(s => console.log(` ${s.product} x${s.quantity} = $${s.cost.toFixed(2)}`));
console.log("❌ Failed:");
failures.forEach(f => console.log(` ${f.error}`));
class OrderException : Exception
{ public OrderException(string msg) : base(msg) { } }
class OutOfStockException : OrderException
{
public string Product { get; }
public int Requested { get; }
public int Available { get; }
public OutOfStockException(string product, int requested, int available)
: base($"'{product}': requested {requested}, only {available} available")
{ Product = product; Requested = requested; Available = available; }
}
class InvalidQuantityException : OrderException
{
public InvalidQuantityException(string product, int qty)
: base($"'{product}': invalid quantity {qty}") { }
}
var inventory = new Dictionary<string, (decimal Price, int Stock)>
{
["Widget"] = (9.99m, 50),
["Gadget"] = (24.99m, 3),
["Gizmo"] = (14.99m, 0),
["Doohickey"] = (4.99m, 100),
};
(string Product, int Qty, decimal Cost) OrderItem(string product, int qty)
{
if (!inventory.ContainsKey(product))
throw new OrderException($"Product '{product}' not found");
if (qty <= 0)
throw new InvalidQuantityException(product, qty);
var item = inventory[product];
if (qty > item.Stock)
throw new OutOfStockException(product, qty, item.Stock);
inventory[product] = (item.Price, item.Stock - qty);
return (product, qty, item.Price * qty);
}
var cart = new[] { ("Widget", 5), ("Gizmo", 2), ("Gadget", 10),
("Doohickey", 20) };
foreach (var (product, qty) in cart)
{
try
{
var result = OrderItem(product, qty);
Console.WriteLine($"✅ {result.Product} x{result.Qty} = {result.Cost:C}");
}
catch (OrderException e)
{
Console.WriteLine($"❌ {e.Message}");
}
}
🎓 Instructor Note: Delivery Guidance
Exercise 1 (Registration) directly applies the lesson concepts: custom hierarchy, data-rich exceptions, and guard clauses. Exercise 2 (Online Store) adds an important real-world pattern: collecting errors instead of stopping at the first one. In a shopping cart, you want to show the user all the problems at once, not one at a time. Walk through the process_cart() function and point out how try/catch inside a loop lets each item fail independently. Challenge fast students: add a PaymentError to Exercise 2 that captures a payment decline reason and error code from a simulated payment gateway.
Summary
🎉 Key Takeaways
- raise/throw creates errors explicitly — Python uses
raise, JS and C# usethrow new - Custom exception classes extend built-in error types — name them
SomethingError(Python/JS) orSomethingException(C#) - Data-rich exceptions carry context (amounts, codes, field names) — callers can make smart recovery decisions
- Exception hierarchies let callers choose specificity — catch the exact type or a broader parent
- Guard clauses validate inputs at function entry — flat code, fail fast, clear happy path
- Throw vs. return: throw for contract violations, return null/result objects for expected outcomes
- JavaScript needs
this.nameset manually in custom error constructors
🚀 What's Next?
You can now catch errors, throw errors, and create your own error types. But what about finding bugs before they become errors? In the next lesson, we'll cover debugging strategies — the tools and techniques for tracking down problems in your code.
🎯 Quick Check
Question 1: What keyword does Python use to throw an error?
Question 2: What must you always set in a JavaScript custom error class constructor?
Question 3: What is a guard clause?