TechByteByByte

Errors and Exception Handling

Learn how to handle failures gracefully in Python using try/except/else/finally, custom exceptions, and retry logic, with realistic examples of handling AI API failures and timeouts.

#Python#Exception Handling#Error Handling#AI#Python for AI

The problem: AI applications depend on files, networks, APIs, and model responses. A file may be missing, a network may time out, or returned JSON may have the wrong shape even when your Python syntax is correct.

What you will learn: You will separate syntax mistakes from runtime exceptions and recover only where recovery makes sense. The goal is not to hide failure; it is to explain it, preserve useful evidence, clean up resources, and avoid unsafe retries that could make the situation worse.


1. What Are Errors?

The goal is not to pretend failures never happen. The goal is to make each failure visible, understandable, and safely handled.

risky operation
   ├── succeeds → continue normally
   └── fails → Python raises an exception

              matching handler?
               ├── yes → recover or explain
               └── no  → stop and show traceback

A traceback is the path of function calls that led to the failure. Read its final line for the exception type and message, then use the earlier lines to find how execution reached the problem.

Two Times an Error Can Appear

An error is something going wrong while Python tries to run your code. Python has two broad categories:

TypeWhat it meansExample
Syntax ErrorCode is not valid Python at all — it can’t even start runningMissing colon, mismatched brackets
Runtime Error (Exception)Code is valid, but something goes wrong while runningDividing by zero, missing dictionary key, failed API call

Grammar Failure Versus Action Failure

A syntax error is like a grammatically broken sentence — it can’t even be read aloud. A runtime error is like a grammatically correct sentence that fails when acted on — “open the door” is valid English, but it fails if there’s no door.


2. Syntax Errors

if True
    print("missing colon")
SyntaxError: expected ':'

Python refuses to run the program at all until this is fixed — it’s caught before execution even begins.


3. Runtime Errors and Exceptions

tokens_used = 100
max_tokens = 0
print(tokens_used / max_tokens)
ZeroDivisionError: division by zero

This code is syntactically valid Python — the error only appears once Python actually tries to execute that line.

🤖 How Is This Used in AI? This distinction matters because runtime errors are exactly what happens when you call an unreliable external service: the code is fine, but the conditions at that moment (no internet, invalid API key, model timeout) cause a failure.


4. try / except

What Is It?

A way to attempt code that might fail, and handle the failure gracefully instead of crashing the whole program.

Why Does It Exist?

You cannot control the outside world. APIs go down. Files go missing. Networks time out. try/except lets your program survive these situations instead of dying immediately.

🧠 Intuition

try is “attempt this, and if something goes wrong, don’t panic — go straight to the plan B I’ve written.”

Real-World Analogy

Think of it like a fire drill: “Try to keep working normally. If the alarm goes off (an exception), stop, follow the evacuation plan (the except block), instead of standing there confused.”

Syntax

try:
    # code that might fail
except SomeErrorType:
    # what to do if it fails

Example

def call_ai_api(prompt):
    if prompt == "":
        raise ValueError("Prompt cannot be empty")
    return f"Response to: {prompt}"

try:
    result = call_ai_api("")
    print(result)
except ValueError as e:
    print(f"API call failed: {e}")

print("Program keeps running after the error.")

Expected Output:

API call failed: Prompt cannot be empty
Program keeps running after the error.

How It Works

  • Python runs the try block.
  • If a ValueError occurs, execution jumps straight to except ValueError instead of crashing the program.
  • as e captures the actual error object so you can inspect or log its message.
  • Code after the whole try/except continues running normally.

💡 Exception Propagation (Bubbling)

If an exception is raised inside a function, and that function doesn’t have a try/except block to catch it, the exception doesn’t just disappear. It bubbles up (propagates) to the code that called the function. If that caller doesn’t catch it, it bubbles up to the caller’s caller, and so on. If it reaches the very top level of your program without being caught, only then does the program crash.

graph TD
    subgraph ExceptionPropagation ["Exception Propagation (Bubbling)"]
        Level3["[ Low-level Tool: call_ai_api() ]<br/>Error raised: ValueError"] -->|bubbles up to| Level2["[ Coordinator: run_agent() ]<br/>No try/except block here"]
        Level2 -->|bubbles up to| Level1["[ Main Script: main() ]<br/>try:<br/>&nbsp;&nbsp;run_agent()<br/>except ValueError:<br/>&nbsp;&nbsp;print('Handled!')"]
    end

🤖 How Is This Used in AI?

import time

def call_llm_with_retry(prompt, max_retries=3):
    for attempt in range(max_retries):
        try:
            # pretend this sometimes raises a timeout
            if attempt < 2:
                raise TimeoutError("Model took too long to respond")
            return f"Response to: {prompt}"
        except TimeoutError as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            time.sleep(1)
    return None

result = call_llm_with_retry("Explain embeddings")
print(result)

Expected Output:

Attempt 1 failed: Model took too long to respond
Attempt 2 failed: Model took too long to respond
Response to: Explain embeddings

This retry loop is a real, common pattern wrapped around actual LLM API calls in production code.

⚠️ Common Beginner Mistake — catching everything blindly:

try:
    result = call_ai_api(prompt)
except:
    pass   # silently swallows EVERY error, including bugs in your own code

A bare except: hides real bugs along with expected failures. Always catch the specific exception type you expect, so genuine programming mistakes still surface loudly instead of vanishing silently.


5. else

What Is It?

Code that runs only if the try block succeeded with no exception.

try:
    response = "valid response"
except ValueError:
    print("failed")
else:
    print("Success:", response)

Expected Output:

Success: valid response

🤖 How Is This Used in AI? Use else for logic that should only run after a successful API call — e.g., only cache a response, or only log “success,” if nothing went wrong.


6. finally

What Is It?

Code that always runs, whether or not an exception occurred — used for cleanup.

try:
    print("calling API...")
    raise ConnectionError("network unreachable")
except ConnectionError as e:
    print(f"Error: {e}")
finally:
    print("Closing connection (always runs).")

Expected Output:

calling API...
Error: network unreachable
Closing connection (always runs).

🤖 How Is This Used in AI? Closing a network connection, releasing a file handle, or logging “request finished” — regardless of whether the call succeeded — belongs in finally.


7. Raising Exceptions

What Is It?

Deliberately triggering an error yourself with raise, when your own code detects something is wrong.

def call_model(prompt, max_tokens):
    if max_tokens <= 0:
        raise ValueError("max_tokens must be greater than 0")
    return f"Response (limited to {max_tokens} tokens): {prompt}"

try:
    call_model("hello", max_tokens=-10)
except ValueError as e:
    print(f"Invalid input: {e}")

Expected Output:

Invalid input: max_tokens must be greater than 0

🧠 Intuition: raise is how a function says “I refuse to continue with this input — something is clearly wrong, and I want the caller to know immediately” rather than silently producing a broken result.

🤖 How Is This Used in AI? Validating inputs before they reach an expensive API call — e.g., rejecting an empty prompt or a negative token limit before you ever spend money calling the model.

💡 Contextual Exceptions

When you raise an exception, always pass a descriptive message containing the contextual details of what failed. In production systems, debug logs often only capture the exception message itself. If your code raises a generic message like raise ValueError("Invalid model"), you won’t know which model name was passed or what the allowed models are.

Instead, write:

VALID_MODELS = {"gpt-4o-mini", "claude-3-5-sonnet"}

def validate_model(model_name: str) -> None:
    if model_name not in VALID_MODELS:
        raise ValueError(
            f"Invalid model name '{model_name}'. Expected one of {VALID_MODELS}."
        )

This single habit saves hours of production troubleshooting time.


8. Custom Exceptions

What Is It?

Your own exception types, created by subclassing Python’s built-in Exception — useful when generic error types (ValueError, TimeoutError) don’t clearly describe what actually went wrong in your application.

class ModelResponseError(Exception):
    """Raised when the AI model returns an invalid or empty response."""
    pass

class RateLimitError(Exception):
    """Raised when the API rate limit has been exceeded."""
    pass

def process_response(response_text):
    if not response_text:
        raise ModelResponseError("Model returned an empty response")
    return response_text.upper()

try:
    process_response("")
except ModelResponseError as e:
    print(f"Handling model error: {e}")
except RateLimitError as e:
    print(f"Handling rate limit: {e}")

Expected Output:

Handling model error: Model returned an empty response

💡 Exception Chaining (raise ... from)

In production AI applications, you will often catch a low-level error (like a network timeout from requests or an SDK-specific error) and wrap it inside a custom exception that your application’s coordinator knows how to handle.

To ensure you don’t lose the original error’s traceback (which is vital for debugging), you should use exception chaining with raise ... from:

import requests

class LLMConnectionError(Exception):
    """Raised when the LLM service is unreachable."""
    pass

def call_raw_api(prompt: str) -> dict:
    try:
        response = requests.post("https://api.example.com/v1/generate", json={"prompt": prompt})
        response.raise_for_status()
        return response.json()
    except requests.exceptions.RequestException as e:
        # Chain the custom exception to the original network error
        raise LLMConnectionError("Failed to reach the AI model service") from e

If this error is raised, the traceback will print both the original RequestException AND your custom LLMConnectionError, preserving the full story of what failed.

🤖 How Is This Used in AI? Real AI codebases define exceptions like ModelResponseError, RateLimitError, InvalidPromptError, or ContextLengthExceededError — this makes error handling specific and readable: you immediately know from the exception name what category of AI-related failure occurred, and can react differently to each.

Key Takeaway: Custom exceptions turn “something broke” into “this specific thing broke,” which is essential once your AI app has many different ways of failing.


9. Exception Handling in AI Applications

Realistic, combined example — a resilient LLM call wrapper:

import time

class ModelTimeoutError(Exception):
    pass

class InvalidPromptError(Exception):
    pass

def call_llm(prompt, max_retries=3):
    if not prompt.strip():
        raise InvalidPromptError("Prompt cannot be empty or whitespace")

    for attempt in range(1, max_retries + 1):
        try:
            print(f"Attempt {attempt}: calling model...")
            if attempt < 3:
                raise ModelTimeoutError("Simulated timeout")
            return f"Model response to: {prompt}"
        except ModelTimeoutError as e:
            print(f"  Timeout: {e}. Retrying...")
            time.sleep(0.5)
        finally:
            print(f"  Attempt {attempt} finished.")

    raise ModelTimeoutError("Model call failed after all retries")

try:
    answer = call_llm("Explain vector databases")
    print("Final answer:", answer)
except InvalidPromptError as e:
    print("Bad input, not retrying:", e)
except ModelTimeoutError as e:
    print("Gave up:", e)

Expected Output:

Attempt 1: calling model...
  Timeout: Simulated timeout. Retrying...
  Attempt 1 finished.
Attempt 2: calling model...
  Timeout: Simulated timeout. Retrying...
  Attempt 2 finished.
Attempt 3: calling model...
  Attempt 3 finished.
Final answer: Model response to: Explain vector databases

This single function demonstrates the realistic shape of production AI error handling: validate input early, retry transient failures, clean up after every attempt, and let genuinely unrecoverable errors bubble up to be handled by the caller.


Decide What Kind of Failure Happened

Not every error should receive the same response:

FailureExampleSensible response
Invalid inputMissing questionExplain what the user must correct
Temporary failureService briefly unavailableRetry a limited number of times
Permission failureWrong API keyStop and fix access; repeated retries will not help
Programming bugMisspelled variableRecord the traceback and fix the code

Catch an exception only where the program can add context, recover, or present a clear message. Avoid except Exception: pass; it hides the evidence needed to find a defect. Retry only operations that are safe to repeat, because repeating a payment or a state-changing tool call can perform the action twice.

Use finally or, preferably, a context manager such as with open(...) when a resource must close even after failure.

Module Summary

You can now distinguish syntax errors from runtime exceptions, handle failures gracefully with try/except/else/finally, deliberately signal problems with raise, and design your own exception types so failures in your AI code are specific and meaningful instead of generic and confusing.

AI Connection

AI applications constantly depend on things outside their control — APIs, networks, models that sometimes misbehave. Exception handling is what separates a fragile script that crashes the moment an API hiccups from a production-grade AI service that retries, logs, and degrades gracefully instead of falling over.

Mini Practice

  1. Write a function that raises a ValueError if temperature is outside the range 0.02.0, and call it inside a try/except that prints a friendly error message.
  2. Write a retry loop (using while or for) around a function that simulates failing twice before succeeding, printing each attempt.
  3. Define a custom exception EmptyDocumentError and raise it from a function that processes a document, if the document is an empty string.
  4. Write a try/except/finally block where the finally clause prints "Request logged" no matter what happens in the try.
  5. Explain, in your own words, why a bare except: is dangerous in an AI application that calls external APIs.

Next: Module 7 — Files and Data — reading/writing text, CSV, and JSON, the data layer underneath every AI pipeline.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed