Week 10 of 16

Build: Custom Exceptions for Prompt Vault

Create exceptions.py with a proper hierarchy and update models.py to speak the app's own error language

Day 48 60 minutes Build

Day 48 of 80

Why Custom Exceptions?

Right now your Prompt class probably raises a generic ValueError when something goes wrong. That works, but it's imprecise. When code that calls your class needs to handle errors, it has to catch all ValueErrors — including ones from completely different parts of Python.

Custom exceptions let you be surgical. Code that uses your app can now write:

example Python
try:
    vault.add(platform="Pika", shot="...", prompt="...")
except InvalidPlatformError as e:
    flash(f"Platform not supported: {e.platform}")
except EmptyFieldError as e:
    flash(f"Please fill in: {e.field_name}")
except PromptVaultError as e:
    flash(f"Vault error: {e}")
Each except clause handles a distinct situation differently. Your Flask route can give the user a specific, useful error message instead of a generic "something went wrong." And because InvalidPlatformError and EmptyFieldError both inherit from PromptVaultError, a catch-all for vault errors still works.

Step 1: Create exceptions.py

Create a new file called exceptions.py in your Prompt Vault project folder. This file defines the entire exception hierarchy for your app.

exceptions.py Python
class PromptVaultError(Exception):
    """Base class for all Prompt Vault errors.

    Catch this if you want to handle any vault error
    without caring about the specific type.
    """
    pass


class InvalidPlatformError(PromptVaultError):
    """Raised when an unrecognized platform is provided."""

    def __init__(self, platform, valid_platforms):
        self.platform = platform
        self.valid_platforms = valid_platforms
        super().__init__(
            f"Unknown platform '{platform}'. "
            f"Valid: {', '.join(valid_platforms)}"
        )


class EmptyFieldError(PromptVaultError):
    """Raised when a required field is empty or whitespace-only."""

    def __init__(self, field_name):
        self.field_name = field_name
        super().__init__(f"'{field_name}' cannot be empty")


class StorageError(PromptVaultError):
    """Raised when reading or writing storage (JSON file, database) fails."""
    pass
Each exception stores extra context as instance attributes. InvalidPlatformError stores self.platform and self.valid_platforms — the calling code can access e.platform to show the user exactly what they typed wrong and what's valid. This is far more useful than a plain string message.
The super().__init__() Call

Always call super().__init__(message) in your custom exception's __init__. This passes the message string up to Exception, which stores it. When someone catches your exception and prints it with str(e) or in an f-string, they get your message. Without that call, the exception would print as an empty string.

Step 2: Update models.py

Now import your new exceptions and use them in Prompt.__init__ and any validation methods. Replace generic ValueError with the specific type.

models.py (updated section) Python
from exceptions import InvalidPlatformError, EmptyFieldError

VALID_PLATFORMS = ["Kling", "Runway", "Veo", "Sora", "Hailuo"]

class Prompt:
    def __init__(self, platform, shot, prompt):
        # Validate before storing anything
        if not platform or not platform.strip():
            raise EmptyFieldError("platform")
        if not shot or not shot.strip():
            raise EmptyFieldError("shot")
        if not prompt or not prompt.strip():
            raise EmptyFieldError("prompt")

        self._validate_platform(platform)

        self.platform = platform.strip()
        self.shot = shot.strip()
        self.prompt = prompt.strip()

    def _validate_platform(self, platform):
        if platform not in VALID_PLATFORMS:
            raise InvalidPlatformError(platform, VALID_PLATFORMS)
Validation happens before any data is stored. If platform is empty, you get EmptyFieldError("platform"). If it's non-empty but unrecognized, you get InvalidPlatformError with the invalid platform and the list of valid ones. No data is half-stored in an invalid state.

Step 3: Test the Exceptions

Create a short scratch script to verify all three exceptions fire correctly.

test_exceptions.py Python
from models import Prompt
from exceptions import PromptVaultError, InvalidPlatformError, EmptyFieldError

print("=== Testing Custom Exceptions ===\n")

# Test 1: Empty field
try:
    p = Prompt("", "golf swing", "cinematic...")
except EmptyFieldError as e:
    print(f"[PASS] EmptyFieldError: {e}")
    print(f"       Field name: {e.field_name}")

# Test 2: Invalid platform
try:
    p = Prompt("Pika", "golf swing", "cinematic...")
except InvalidPlatformError as e:
    print(f"[PASS] InvalidPlatformError: {e}")
    print(f"       Bad platform: {e.platform}")
    print(f"       Valid options: {e.valid_platforms}")

# Test 3: Catch via base class
try:
    p = Prompt("Pika", "golf swing", "cinematic...")
except PromptVaultError as e:
    print(f"[PASS] Caught via base class: {type(e).__name__}")

# Test 4: Valid prompt — no exception
try:
    p = Prompt("Kling", "Golf swing follow-through", "Cinematic slow motion...")
    print(f"[PASS] Valid prompt created: [{p.platform}] {p.shot}")
except PromptVaultError as e:
    print(f"[FAIL] Unexpected error: {e}")
Run this with python test_exceptions.py. All four tests should pass. Test 3 proves that InvalidPlatformError IS a PromptVaultError — the hierarchy works.

Why This Is Better

Precision vs. Noise

Before: your models.py raised ValueError("Pika is not a valid platform"). The calling code had to catch all ValueErrors — including ones that might come from Python's built-in functions deep in your stack.

After: your models.py raises InvalidPlatformError("Pika", VALID_PLATFORMS). The calling code catches exactly that, nothing else. The Flask route can access e.platform to show the user what they typed. The error carries structured data, not just a message string.

This is the difference between "something went wrong" and "you tried to use Pika, which isn't supported; here are the valid options: Kling, Runway, Veo, Sora, Hailuo."

Don't Overdo It

You don't need a custom exception for every possible error. Create them when: (1) calling code needs to handle the error differently based on its type, or (2) the error needs to carry structured data beyond a message string. If a generic ValueError or TypeError works, use it.

End of Day Checklist

Tomorrow

Day 49 adds logging throughout the app. You'll create logger.py — a central logger factory — and add logger.info(), logger.warning(), and logger.error() calls to models.py, vault.py, and wherever else things happen. By end of day, your app will write a timestamped record of everything it does.