Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
โฑ๏ธ 6 min read

Exception Handling

Good exception handling makes your code robust without cluttering it. In machine coding rounds, custom exceptions signal domain-aware design. Throwing RuntimeException("something went wrong") everywhere signals the opposite.

Why this matters: Interviewers notice when you throw meaningful, domain-specific exceptions (InsufficientBalanceException, SlotUnavailableException) vs. when you let NPEs crash your demo or swallow errors silently.


Prerequisites


The Exception Hierarchy Strategy

classDiagram
    class BaseException {
        -message: String
        -errorCode: String
    }
    class BusinessException {
        +Recoverable domain errors
    }
    class SystemException {
        +Infrastructure failures
    }
    class InsufficientBalanceException {
        -available: Money
        -required: Money
    }
    class ProductNotFoundException {
        -productId: String
    }
    class DatabaseUnavailableException {
        -retryAfter: Duration
    }

    BaseException <|-- BusinessException
    BaseException <|-- SystemException
    BusinessException <|-- InsufficientBalanceException
    BusinessException <|-- ProductNotFoundException
    SystemException <|-- DatabaseUnavailableException

Two categories:


Custom Exceptions Done Right

// Base exception for the domain
public abstract class WalletException extends RuntimeException {
    private final String errorCode;

    protected WalletException(String message, String errorCode) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

// Specific: carries context about what went wrong
public class InsufficientBalanceException extends WalletException {
    private final Money available;
    private final Money required;

    public InsufficientBalanceException(Money available, Money required) {
        super(String.format("Insufficient balance: have %s, need %s", available, required),
              "WALLET_001");
        this.available = available;
        this.required = required;
    }

    public Money getAvailable() { return available; }
    public Money getRequired() { return required; }
    public Money getDeficit() { return required.subtract(available); }
}

public class DuplicateTransactionException extends WalletException {
    private final String transactionId;

    public DuplicateTransactionException(String transactionId) {
        super("Transaction already processed: " + transactionId, "WALLET_002");
        this.transactionId = transactionId;
    }

    public String getTransactionId() { return transactionId; }
}

public class WalletFrozenException extends WalletException {
    private final String reason;
    private final Instant frozenSince;

    public WalletFrozenException(String walletId, String reason, Instant frozenSince) {
        super("Wallet " + walletId + " is frozen: " + reason, "WALLET_003");
        this.reason = reason;
        this.frozenSince = frozenSince;
    }
}

// Usage: meaningful, catchable, debuggable
public class WalletService {
    public void debit(String walletId, Money amount, String idempotencyKey) {
        Wallet wallet = repository.findById(walletId)
            .orElseThrow(() -> new WalletNotFoundException(walletId));

        if (wallet.isFrozen()) {
            throw new WalletFrozenException(walletId, wallet.getFreezeReason(), wallet.getFrozenAt());
        }

        if (transactionLog.exists(idempotencyKey)) {
            throw new DuplicateTransactionException(idempotencyKey);
        }

        if (wallet.getBalance().isLessThan(amount)) {
            throw new InsufficientBalanceException(wallet.getBalance(), amount);
        }

        wallet.debit(amount);
        transactionLog.record(idempotencyKey, amount);
    }
}
class WalletError(Exception):
    """Base exception for wallet domain."""
    def __init__(self, message: str, error_code: str):
        super().__init__(message)
        self.error_code = error_code

class InsufficientBalanceError(WalletError):
    def __init__(self, available: Decimal, required: Decimal):
        self.available = available
        self.required = required
        self.deficit = required - available
        super().__init__(
            f"Insufficient balance: have {available}, need {required}",
            "WALLET_001"
        )

class DuplicateTransactionError(WalletError):
    def __init__(self, transaction_id: str):
        self.transaction_id = transaction_id
        super().__init__(
            f"Transaction already processed: {transaction_id}",
            "WALLET_002"
        )

class WalletFrozenError(WalletError):
    def __init__(self, wallet_id: str, reason: str):
        self.wallet_id = wallet_id
        self.reason = reason
        super().__init__(f"Wallet {wallet_id} frozen: {reason}", "WALLET_003")

class WalletNotFoundError(WalletError):
    def __init__(self, wallet_id: str):
        self.wallet_id = wallet_id
        super().__init__(f"Wallet not found: {wallet_id}", "WALLET_004")

# Usage
class WalletService:
    def debit(self, wallet_id: str, amount: Decimal, idempotency_key: str):
        wallet = self._repo.find_by_id(wallet_id)
        if wallet is None:
            raise WalletNotFoundError(wallet_id)
        
        if wallet.is_frozen:
            raise WalletFrozenError(wallet_id, wallet.freeze_reason)
        
        if self._tx_log.exists(idempotency_key):
            raise DuplicateTransactionError(idempotency_key)
        
        if wallet.balance < amount:
            raise InsufficientBalanceError(wallet.balance, amount)
        
        wallet.debit(amount)
        self._tx_log.record(idempotency_key, amount)
#include <stdexcept>
#include <string>

class WalletException : public std::runtime_error {
    string errorCode_;
public:
    WalletException(const string& message, const string& code)
        : runtime_error(message), errorCode_(code) {}
    const string& errorCode() const { return errorCode_; }
};

class InsufficientBalanceException : public WalletException {
    double available_, required_;
public:
    InsufficientBalanceException(double available, double required)
        : WalletException(
            "Insufficient balance: have " + to_string(available) +
            ", need " + to_string(required), "WALLET_001"),
          available_(available), required_(required) {}
    
    double available() const { return available_; }
    double required() const { return required_; }
    double deficit() const { return required_ - available_; }
};

class WalletFrozenException : public WalletException {
    string walletId_, reason_;
public:
    WalletFrozenException(const string& walletId, const string& reason)
        : WalletException("Wallet " + walletId + " frozen: " + reason, "WALLET_003"),
          walletId_(walletId), reason_(reason) {}
};

// Usage
void WalletService::debit(const string& walletId, double amount, const string& key) {
    auto wallet = repo_->findById(walletId);
    if (!wallet) throw WalletNotFoundException(walletId);
    if (wallet->isFrozen()) throw WalletFrozenException(walletId, wallet->freezeReason());
    if (wallet->balance() < amount)
        throw InsufficientBalanceException(wallet->balance(), amount);
    
    wallet->debit(amount);
}

Error Handling Anti-Patterns

Anti-Pattern Why Itโ€™s Bad Fix
catch (Exception e) {} Silently swallows errors Catch specific exceptions, log or rethrow
throw new RuntimeException("error") No context, uncatchable selectively Custom exception with relevant data
Returning null on failure Caller forgets to check, NPE later Throw exception or return Optional
Wrapping every line in try/catch Clutters code, hides flow Let exceptions propagate, catch at boundaries
Using exceptions for flow control Expensive, hard to follow Use conditionals for expected cases

Checked vs Unchecked (Java-Specific)

Checked (extends Exception) Unchecked (extends RuntimeException)
Caller forced to handle or declare Caller not forced to handle
Use for recoverable conditions Use for programming errors and unrecoverable failures
Makes API contract explicit Keeps code clean
Example: IOException, SQLException Example: NullPointerException, IllegalArgumentException

In LLD interviews: Use unchecked (RuntimeException) for domain exceptions. Checked exceptions add noise in a 90-minute session.


When to Use vs When to Avoid Exceptions

Use Exceptions For Use Return Values For
Unexpected, exceptional situations Expected alternative outcomes
Errors that require caller attention Optional results (empty list, null/None)
Contract violations Boolean success/failure for simple ops
Cross-layer error propagation Performance-critical hot paths

Interview Questions

  1. โ€œHow many custom exceptions should you create?โ€ โ€“ One per distinct error condition that callers need to handle differently. In a wallet system: insufficient funds, wallet frozen, duplicate transaction. Donโ€™t create one for every possible failure mode.

  2. โ€œShould you include context data in exceptions?โ€ โ€“ Yes. InsufficientBalanceException should carry available and required amounts so the caller can provide useful error messages or make decisions.

  3. โ€œWhere should you catch exceptions?โ€ โ€“ At domain boundaries: API layer catches domain exceptions and converts to HTTP status codes. Service layer catches infrastructure exceptions and wraps them in domain exceptions. Donโ€™t catch in the middle of business logic unless you can meaningfully handle it.

  4. โ€œException vs return code?โ€ โ€“ Exceptions for unexpected failures that interrupt normal flow. Return codes (or Result types) for expected alternative outcomes. Donโ€™t use exceptions for expected โ€œnot foundโ€ cases that happen frequently.


See It in Action

Exception design is part of every LLD problem. Payment Wallet and Vending Machine demonstrate custom exception hierarchies.

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access