Part 4: Advanced Debugging Techniques

Introduction

Basic debugging works for synchronous code, but modern Python applications use async/await, handle exceptions across multiple layers, work with decorators, and run background tasks. I learned advanced debugging the hard wayβ€”spending hours debugging async race conditions that basic breakpoints couldn't catch.

This article covers advanced techniques: debugging async code, exception strategies, multi-threaded debugging, working with decorators and context managers, and using logpoints for production-like scenarios.

Debugging Async/Await Code

Async Python code requires different debugging approaches than synchronous code.

Basic Async Debugging

import asyncio
from aiohttp import ClientSession

async def fetch_user_data(user_id):
    async with ClientSession() as session:
        url = f"https://api.example.com/users/{user_id}"
        async with session.get(url) as response:  # ← Breakpoint here
            data = await response.json()
            return data

async def main():
    user = await fetch_user_data(123)  # ← Or breakpoint here
    print(user)

if __name__ == "__main__":
    asyncio.run(main())

Debug configuration for async code:

Debugging Concurrent Async Tasks

Challenge: Multiple tasks run concurrently. Breakpoint hits multiple times.

Solution: Conditional breakpoint

Real Example: Debugging Race Conditions

I built an async service that fetched data from multiple APIs and combined results. Sometimes results were incomplete.

Problem: When requesting user ID 1 three times concurrently, cache check fails for all three because none have finished yet.

Debugging steps:

  1. Set breakpoint in fetch_user() at cache check

  2. Use watch expressions:

  3. Call stack shows:

All three tasks check cache before any finishβ€”race condition!

Fix with async lock:

Debugging Event Loop Issues

Exception Debugging Strategies

Enable Exception Breakpoints

VS Code can pause when exceptions are raised:

  1. Run and Debug view β†’ Breakpoints panel

  2. Check "Raised Exceptions" or "Uncaught Exceptions"

With "Raised Exceptions" enabled: Pauses at line 3 when exception occurs

With "Uncaught Exceptions" only: Doesn't pause (exception is caught)

Real Scenario: Silent Failures

Without exception breakpoint: Failures are invisible

With exception breakpoint: Pauses on each exception, showing exactly what failed

Exception Filtering

Don't want to pause on every exception, only specific ones:

Only pauses on ValueError and KeyError, ignores others.

Debugging Exception Chains

Debug console at exception:

Debugging with Decorators

Decorators can make debugging tricky. How do you step into decorated functions?

Basic Decorator Debugging

Step 1: Breakpoint in wrapper at result = func(*args, **kwargs) Step 2: Press F11 (Step Into) to enter calculate_price

Debugging Decorator-Heavy Code

Debug strategy:

  1. Breakpoint in innermost decorator wrapper (rate_limit)

  2. Step Into (F11) to reach actual function

  3. Use "justMyCode": false in launch.json to see into decorators

Real Example: Debugging FastAPI Dependency Injection

Debugging FastAPI endpoints:

Set breakpoints in:

  • get_db() to see database connection

  • get_current_user() to see user loading

  • get_profile() for endpoint logic

Logpoints for Production-Like Debugging

Logpoints let you output messages without stopping executionβ€”perfect for scenarios where stopping would change behavior.

Debugging Concurrent Requests

Why logpoints instead of breakpoints?

  • Server handles 100 requests/second

  • Regular breakpoints would pause each request

  • Logpoints show flow without stopping

Debug Console output:

Debugging Websocket Connections

Regular breakpoints would disconnect clients. Logpoints show message flow without interruption.

Logpoints with Conditionals

Logpoints can include expressions:

Or combined with conditional breakpoints:

Debugging Multi-Threaded Code

Python threading can be debugged in VS Code with care.

Basic Threading Debug

Breakpoint behavior: Pauses ONE thread, others continue

Call stack shows all threads:

Debugging Thread-Safe Code

Watch expressions:

Debugging Context Managers

Step Into at with statement enters __enter__

Step Out after with block enters __exit__

Advanced Techniques

Debugging Generators

Each iteration hits the breakpoint. Use conditional or hit count to debug specific iterations.

Debugging Comprehensions

List comprehensions run in single expressionβ€”hard to debug:

Post-Mortem Debugging

When code crashes, inspect the state at crash time:

Or use VS Code's exception breakpoints to pause automatically.

Real-World Example: Debugging Async API Client

Debugging strategy:

  1. Logpoint in fetch(): See each URL being requested

  2. Breakpoint in fetch_all(): Inspect task creation

  3. Conditional breakpoint when errors occur: len(errors) > 0

  4. Exception breakpoint: Catch any unhandled exceptions

  5. Watch expressions:

Best Practices

1. Use Logpoints for High-Frequency Code

2. Conditional Breakpoints for Specific Cases

3. Exception Breakpoints for Silent Failures

Enable "Raised Exceptions" to catch swallowed exceptions in try/except blocks.

4. justMyCode Setting

What's Next

You now know:

  • Debugging async/await and concurrent code

  • Exception breakpoints and strategies

  • Working with decorators and context managers

  • Logpoints for production-like scenarios

  • Multi-threaded debugging basics

In Part 5: Debug Configurations and Production Debugging, you'll learn creating comprehensive launch.json configurations, debugging tests, remote debugging, and investigating production issues.


Based on debugging complex async microservices, multi-threaded applications, and production Python systems in VS Code.

Last updated