Part II: The Professional's Toolkit
7.4 Debuggers: The Underrated Powerhouse
You're staring at a Django view that handles user registration. The documentation says it sends a welcome email, but users report they're not receiving it. You add a print statement before the email sending codeâit prints. You add one afterâit prints. The email function itself has prints showing it's being called with the right arguments. Everything prints. Nothing breaks. No errors in logs. But no emails arrive.
You've just hit the wall that print statement debugging always hits: you can see that code runs, but you can't see what it does, what state it modifies, what exceptions it catches and swallows, or what it returns. You're observing breadcrumbs, not the actual journey.
This is the moment you need a debugger.
Why debuggers are more powerful than logging
Debuggers offer something fundamentally different from logging: they let you pause reality. When execution stops at a breakpoint, you're standing inside the running program. You can inspect every variable in scope, evaluate arbitrary expressions, modify values on the fly, and then resumeâor step forward one instruction at a time, watching exactly how state transforms.
Here's what makes this transformative for understanding unfamiliar code:
1. You don't need to predict what to log. With print statements, you must anticipate which variables matter. In unfamiliar code, you don't know yet. You add five prints, re-run, discover you needed different variables, add five more, re-run again. Each cycle takes 30 seconds to minutes. With a debugger, you set one breakpoint and inspect everything when you get there.
2. You can explore interactively. At a breakpoint, you can call functions, check object attributes, evaluate conditionalsâanything you could do in the code itself. Need to know what user.get_profile() returns? Just type it in the debug console. With logging, you'd need to modify code, save, restart, and re-trigger the execution path.
3. You see the exact execution path. The call stack shows you precisely how execution reached this pointâevery function call from the entry point to your current location. This is your execution roadmap through the codebase. Print statements show you isolated moments; the call stack shows you the journey.
4. You can set conditions without code changes. A breakpoint can trigger only when user.email.endswith('@gmail.com') or when request_count > 100. You're filtering execution paths without touching source code, without deployments, without restarting servers in some cases.
Let's see this in practice with that email problem. Instead of print statements, you set a breakpoint on the line that calls send_email(). When it hits, you examine the call stack and discover the function is actually called from inside a try-except block three levels up that catches all exceptions and logs them at DEBUG levelâwhich you don't have enabled. The function raises SMTPAuthenticationError, gets caught, and execution continues as if nothing happened. Your print statements all ran. The email function was "called." But the email never sent.
This took two minutes with a debugger. It could take hours with loggingâif you ever found it at all.
The call stack: Your execution roadmap
When your breakpoint hits, the most valuable thing you can examine isn't local variablesâit's the call stack. The call stack is the sequence of function calls that led to this moment. It's your map through the codebase.
Here's what a typical call stack looks like when debugging a Django request:
â myapp/views.py:45 in register_user()
myapp/forms.py:23 in save()
django/views/generic/edit.py:145 in form_valid()
django/views/generic/edit.py:89 in post()
django/core/handlers/base.py:181 in _get_response()
django/core/handlers/base.py:127 in get_response()
django/middleware/csrf.py:78 in process_request()
django/core/handlers/base.py:76 in inner()
[... more middleware ...]
django/core/handlers/wsgi.py:142 in __call__()
This arrow (â) points to where execution is pausedâyour breakpoint. Each line above it is a frame: a function that called the one below it. Reading bottom to top, you see the complete execution path from HTTP request entry to your current location.
This is crucial for unfamiliar codebases because it answers the question: "How did we get here?" You might set a breakpoint in a utility function, not knowing where it's called from. The call stack immediately shows you: it's called from form validation, which is called from a generic view, which is invoked by Django's routing system. You've just learned the architecture of the request flow without reading documentation.
Each frame in the stack has its own local variables and state. You can navigate up and down the stack, inspecting the context at each level. In the Django example above, you might step up to the form_valid() frame to see what the form instance looks like, then up to process_request() to inspect the middleware's modifications to the request object. You're traveling through the execution history, examining state at different moments in time.
This is why debuggers are so powerful for tracing: they let you navigate both forward (step through code) and backward (examine the call stack). You're not just seeing executionâyou're exploring it.
Conditional breakpoints: Surgical precision without code changes
Here's a common problem: you need to trace execution, but only in a specific case. Maybe a function is called 1000 times per request, but only the 847th call exhibits the bug. Or you need to trace only requests from a specific user, only POST requests, or only when a variable has a particular value.
You could add an if statement to your code:
if user.id == 42 and request.method == 'POST':
breakpoint() # Only for debugging - remember to remove!
But now you've modified source code. You need to remember to remove this. If you're debugging third-party code, you're editing your virtualenv, which means you might lose these changes or accidentally commit them. And you need to restart your server.
Conditional breakpoints solve this: you tell the debugger to pause only when a condition is true, without touching any code. In VS Code, you right-click a breakpoint and select "Edit Breakpoint," then enter:
user.email.endswith('@gmail.com') and request.method == 'POST'
Now this breakpoint triggers only when both conditions are met. The debugger evaluates the expression in the local context every time execution reaches that line. No code changes. No restart. No manual cleanup.
This transforms how you trace execution in production-like scenarios. You can set a breakpoint in a high-traffic function with the condition request.path == '/admin/users/import/' to catch only admin imports. Or len(items) > 100 to catch only bulk operations. Or 'error' in result to catch only failures.
The key insight here is that conditional breakpoints give you the precision of code instrumentation without the cost. You're filtering execution paths at runtime, surgically, reversibly, and without deployment overhead.
7.4.1 Python/Django Debugging
Let's make this concrete. You're joining a Django project with authentication, payments, and background tasks. The team lead mentions "the login flow is complex." You need to understand it quickly. Here's how you use debuggers to trace it.
VS Code Python debugger configuration
First, you need VS Code configured to debug Django. Create or modify .vscode/launch.json in your project root:
{
"version": "0.2.0",
"configurations": [
{
"name": "Django: Debug Server",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/manage.py",
"args": ["runserver", "--noreload"],
"django": true,
"justMyCode": false,
"console": "integratedTerminal"
}
]
}
Let's understand each piece:
-
"justMyCode": falseis crucial. By default, VS Code skips stepping into library code. With this set tofalse, you can step into Django's internals, third-party packages, and any code in your virtualenv. This is essential for tracing through unfamiliar codebases where the interesting logic might be in middleware you didn't write. -
"--noreload"prevents Django's auto-reloader from spawning a subprocess. The reloader watches for file changes and restarts the server, but it breaks debugger attachment. You'll manually restart when needed. -
"django": trueenables Django-specific debugging features, like template debugging and better SQL query inspection.
With this configuration, press F5 to start debugging. The server runs under debugger control. Now set a breakpoint anywhereâin your views, in Django's internals, in third-party packagesâand when execution hits it, VS Code pauses and shows you the full context.
Setting breakpoints in third-party code
Here's where debuggers shine for understanding unfamiliar code: you can set breakpoints anywhere, even in code you can't modify.
Let's trace that complex login flow. You know the login URL is /accounts/login/, but you don't know which view handles it. Start by setting a breakpoint in Django's URL resolver. Open your virtualenv's Django installation (VS Code will help you navigate there when you search for files) and find django/core/handlers/base.py. Search for the _get_response methodâthis is where Django resolves URLs to views. Set a breakpoint on the line that looks like:
response = callback(request, **callback_kwargs)
This is where Django calls your view function (or third-party view). When you trigger a login request, execution pauses here. Now inspect callback:
callback = <function LoginView.as_view.<locals>.view at 0x...>
callback_kwargs = {}
So login is handled by LoginView. But where is that defined? Look at the call stackâa few frames up, you'll see something like:
django_allauth/account/views.py:123 in LoginView.dispatch()
Aha! This project uses django-allauth. You didn't know that from reading code. You discovered it by tracing execution. Now you can navigate to that file (VS Code lets you click stack frames to jump to the code) and set breakpoints in LoginView.dispatch(), LoginView.get(), and LoginView.post().
This is the power of breakpoints in third-party code: you discover architecture by following execution, not by reading documentation that might be outdated or absent.
A common mistake: You set a breakpoint in a third-party library, trigger your action, but the breakpoint doesn't hit. Why? Either that code path isn't executing (your assumption about the flow was wrong), or the breakpoint is in the wrong location (maybe on a comment line or blank line). To debug your debugging: set a breakpoint in your own view firstâsomewhere you're absolutely certain executesâthen step into library calls from there. This guarantees you're following the actual execution path.
Using pdb for terminal-based exploration
VS Code is excellent for GUI-based debugging, but sometimes you're SSHed into a server, or running code in a Docker container without GUI access, or you just want a lightweight exploration tool. That's when you use pdb, Python's built-in debugger.
Insert this line anywhere in your code where you want to pause:
import pdb; pdb.set_trace()
When execution reaches this line, Python drops into an interactive debugger prompt in your terminal:
> /app/myapp/views.py(45)register_user()
-> email = send_welcome_email(user)
(Pdb)
You're now in control. Common commands:
-
l(list): Show surrounding code -
n(next): Execute current line, move to next -
s(step): Step into function calls -
c(continue): Resume execution until next breakpoint -
p variable_name: Print a variable's value -
pp variable_name: Pretty-print complex objects -
w(where): Show call stack -
u(up): Move up one frame in the call stack -
d(down): Move down one frame
Here's a real exploration session. You're trying to understand why emails aren't sending:
(Pdb) l
40 def register_user(request):
41 form = RegistrationForm(request.POST)
42 if form.is_valid():
43 user = form.save()
44 import pdb; pdb.set_trace()
45 -> email = send_welcome_email(user)
46 return redirect('registration_complete')
47 return render(request, 'register.html', {'form': form})
(Pdb) p user
<User: john@example.com>
(Pdb) p user.email
'john@example.com'
(Pdb) s # step into send_welcome_email
--Call--
> /app/myapp/email.py(12)send_welcome_email()
-> def send_welcome_email(user):
(Pdb) l
12 -> def send_welcome_email(user):
13 try:
14 send_mail(
15 subject='Welcome!',
16 message=f'Welcome {user.username}',
17 from_email=settings.DEFAULT_FROM_EMAIL,
18 recipient_list=[user.email]
19 )
20 except Exception as e:
21 logger.debug(f"Email failed: {e}")
22 return True
(Pdb) n
(Pdb) n
(Pdb) n
(Pdb) n # stepping through the try block
> /app/myapp/email.py(20)send_welcome_email()
-> except Exception as e:
(Pdb) n
> /app/myapp/email.py(21)send_welcome_email()
-> logger.debug(f"Email failed: {e}")
(Pdb) p e
SMTPAuthenticationError(535, b'Authentication failed')
There it is. The exception is caught and logged at DEBUG level. You'd never see this with print statements because the exception is swallowed. The function returns True even though it failed. Pdb revealed this in 30 seconds.
Important pdb tip: If you're stepping through code and realize you stepped into a function you don't care about, use r (return) to execute until the function returns. This saves you from stepping through dozens of lines of library code you don't need to inspect.
The breakpoint() function (Python 3.7+)
Python 3.7 introduced a cleaner way to trigger the debugger:
breakpoint()
That's it. No imports. It's a built-in function that calls sys.breakpointhook(), which defaults to launching pdb. Why is this better than import pdb; pdb.set_trace()?
-
Cleaner code: One word instead of a statement and function call
-
Configurable: You can change what debugger it launches via the
PYTHONBREAKPOINTenvironment variable -
Disableable: Set
PYTHONBREAKPOINT=0to disable all breakpoint() calls without removing them from code
This last point is significant. You can leave breakpoint() calls in code during development, then disable them in production without modifying source:
PYTHONBREAKPOINT=0 gunicorn myapp.wsgi
All breakpoint() calls become no-ops. This is safer than pdb.set_trace(), which would crash production code (pdb requires a terminal TTY, which production servers don't have).
For local development, you might want to use a more powerful debugger than pdb:
# Use ipdb (IPython-enhanced pdb)
pip install ipdb
PYTHONBREAKPOINT=ipdb.set_trace python manage.py runserver
# Or use web-pdb (browser-based interface)
pip install web-pdb
PYTHONBREAKPOINT=web_pdb.set_trace python manage.py runserver
Now all your breakpoint() calls launch your preferred debugger. This is the flexibility breakpoint() provides over hardcoded pdb.set_trace().
Debugging Django middleware chains
Django's middleware system is notoriously hard to understand from reading code alone. Middleware wraps your views in layers, each processing the request before your view runs and processing the response after. Understanding the execution order and how middleware modifies state requires tracing actual execution.
Here's a Django settings.py middleware configuration:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'myapp.middleware.CustomLoggingMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
When a request arrives, Django calls each middleware's process_request() method top to bottom, then your view, then each middleware's process_response() method bottom to top. But some middleware might short-circuit the chain, some might modify the request object, and some might handle exceptions. Reading the documentation is abstract. Tracing reveals reality.
Set a breakpoint in your view function and trigger a request. When it hits, examine the call stack:
â myapp/views.py:34 in dashboard()
django/core/handlers/base.py:181 in _get_response()
django/core/handlers/base.py:133 in inner()
myapp/middleware.py:15 in __call__()
django/core/handlers/base.py:133 in inner()
django/contrib/messages/middleware.py:46 in __call__()
django/core/handlers/base.py:133 in inner()
django/contrib/auth/middleware.py:61 in __call__()
[... more middleware frames ...]
Each middleware's __call__ method wraps the next. This visual confirms the execution order and shows exactly how deeply nested your view call is. Now step up the stack frame by frame, inspecting request at each level. Watch how AuthenticationMiddleware adds request.user, how SessionMiddleware populates request.session, how each middleware augments the request object.
To trace middleware response processing, set a breakpoint just before your view returns, then step out (or continue) and watch the call stack unwind through each middleware's response handling.
Tracing ORM query generation
Django's ORM is famously magical: you write Python, it generates SQL. But when you encounter an N+1 query problem or need to optimize a complex queryset, you need to understand how your Python translates to SQL.
Say you have this view:
def author_list(request):
authors = Author.objects.prefetch_related('books').all()
return render(request, 'authors.html', {'authors': authors})
You suspect this generates inefficient queries, but you're not sure. Set a breakpoint on the render() lineâbefore the queryset is evaluated (Django querysets are lazy). Now step into the render() call. The debugger will eventually step into template rendering, which iterates over authors, which triggers query evaluation.
When you see database activity in logs or notice execution slow down, pause the debugger (most debuggers let you manually break). Check the call stack:
â django/db/models/query.py:387 in __iter__()
django/db/models/sql/query.py:256 in resolve_expression()
django/db/backends/utils.py:66 in execute()
django/db/backends/postgresql/base.py:54 in execute()
You're inside Django's query execution. Step up to the __iter__() frame and inspect self.query:
(Pdb) print(self.query)
SELECT "author"."id", "author"."name" FROM "author"
Step through and watch for more query execution. You'll see the prefetch query:
SELECT "book"."id", "book"."title", "book"."author_id"
FROM "book"
WHERE "book"."author_id" IN (1, 2, 3, 4, 5)
This confirms your prefetch_related is workingâDjango issued exactly two queries (one for authors, one for all their books), not N+1 queries. You discovered this by tracing execution, not by hoping your mental model is correct.
A powerful technique: Set a conditional breakpoint in django/db/backends/utils.py on the execute() method with condition 'SELECT' in sql. This breakpoint triggers for every SELECT query Django runs. By examining the call stack each time it hits, you build a complete map of which code causes which queries. This is invaluable for query optimization.
7.4.2 JavaScript/TypeScript Debugging
JavaScript debugging has evolved dramatically. Modern browsers and VS Code provide powerful debugging experiences that rival desktop IDEs. Let's explore how to trace execution in JavaScript codebasesâparticularly critical since JavaScript often involves asynchronous code, complex build tooling, and browser-specific behavior that's hard to reason about statically.
Chrome DevTools: The complete execution theater
Chrome DevTools isn't just a debuggerâit's a complete execution inspection environment. Press F12 in Chrome, click the Sources tab, and you have access to every script running on the page, with full debugging capabilities.
Let's trace a real scenario: you're debugging a React application where a form submission isn't working. You know the button click is registered (you can see it in the console), but the server never receives the request. Time to trace.
Open DevTools, navigate to the Sources panel, and find your form component file. You'll see your actual source code, not transpiled output (thanks to source maps, which we'll cover shortly). Set a breakpoint on the form's submit handler:
const handleSubmit = async (e) => {
e.preventDefault(); // â Set breakpoint here
const data = { name: formState.name, email: formState.email };
const response = await fetch("/api/users", {
method: "POST",
body: JSON.stringify(data),
});
};
Click the submit button. Chrome pauses at your breakpoint. Now you see the execution context:
-
Scope panel: Shows local variables (
e,data), closure variables, and global scope -
Call stack: Shows how execution reached this point
-
Watch expressions: Lets you track specific expressions as you step through code
-
Console: Still accessibleâevaluate arbitrary expressions in the current context
Step through (F10) and watch what happens. You step over e.preventDefault(), then const data = { ... }. When you step over the fetch() line, notice something: the breakpoint doesn't move forward. Why? The fetch() call returns a Promise immediatelyâthe actual network request happens asynchronously.
This is crucial: async debugging requires understanding when code is synchronous vs. asynchronous. Click "Step into" (F11) repeatedly and watch the call stack. You'll see execution jump into browser internals, then return to your code when the Promise resolves. But something's wrongâwhen the Promise settles, you're not in a .then() handler or after an await. You're stuck.
Check the call stack. You see:
(anonymous) @ form.js:23
Array.forEach
processFormQueue @ utils.js:56
Someone added your form to a processing queue, and there's a forEach loop processing forms. When your async handler returns a Promise, the forEach doesn't waitâit immediately processes the next form. Your form submission's Promise is created but never awaited. The request might start but isn't guaranteed to complete before the page navigates.
You discovered this by tracing execution flow, watching the async boundary, and inspecting the call stack. Reading the code statically wouldn't reveal this subtle async bug.
Key Chrome DevTools features for tracing:
-
Blackboxing: Right-click a script in the call stack and select "Blackbox script." Now when stepping, the debugger skips over this script. This is essential for ignoring library code (React internals, lodash, etc.) and focusing on your application code.
-
Event Listener Breakpoints: In the Sources panel's right sidebar, expand "Event Listener Breakpoints." Check "Mouse > click" to pause on any click event. This helps you discover where event handlers are registered without reading code. When it pauses, the call stack shows you which handler is executing.
-
XHR/fetch breakpoints: Also in the right sidebar, expand "XHR/fetch Breakpoints." Add a breakpoint with URL substring
/api/users. Now any fetch or XHR to a URL containing that string will pause execution. This is perfect for tracing API calls without knowing where they originate in the code. -
Pause on exceptions: Click the pause button icon at the top of the Sources panel. This makes the debugger pause whenever an exception is thrownâeven if it's caught. Critical for finding swallowed errors.
VS Code Node.js debugger setup
For server-side JavaScript (Node.js, Express, Next.js API routes), you'll debug in VS Code. The setup is similar to Python debuggingâcreate .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Node App",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/server.js",
"restart": true,
"console": "integratedTerminal"
}
]
}
Key configuration options:
-
"skipFiles": ["<node_internals>/**"]prevents stepping into Node.js core modules (likehttp,fs). Add patterns for libraries you want to skip:["<node_internals>/**", "**/node_modules/**"]skips all dependencies. -
"restart": trueautomatically restarts the debugger when you save files. Combined with nodemon, this gives you a smooth development experience. -
"console": "integratedTerminal"runs your app in VS Code's terminal, preserving console.log output alongside debugger output.
For debugging TypeScript directly (not compiled JS), add:
{
"type": "node",
"request": "launch",
"name": "Debug TypeScript",
"runtimeArgs": ["-r", "ts-node/register"],
"args": ["${workspaceFolder}/src/server.ts"],
"skipFiles": ["<node_internals>/**"]
}
This uses ts-node to run TypeScript files directly. Install it: npm install -D ts-node.
Now you can set breakpoints directly in .ts files and debug them as if they were source codeâno separate compilation step.
Debugging attached processes: You can also attach the debugger to an already-running Node process. This is useful for debugging applications started by other tools (like npm start or docker-compose):
{
"type": "node",
"request": "attach",
"name": "Attach to Node Process",
"port": 9229,
"restart": true
}
Start your Node app with the inspect flag: node --inspect server.js. It listens on port 9229 for debugger connections. In VS Code, press F5 and select "Attach to Node Process." Now you're debugging a live process without restarting itâbreakpoints work, variables are inspectable, everything.
Source maps: Debugging TypeScript as if it were source code
When you write TypeScript (or JSX, or use Babel), your code is transpiled to JavaScript before execution. Without source maps, debuggers show you the generated JavaScript, not your original source. This is painfulâtrying to map generated code back to source mentally is error-prone and slow.
Source maps solve this by providing a mapping from generated code to original source. When a source map is present, the debugger automatically shows your TypeScript/JSX source, sets breakpoints in the correct locations, and displays variables with their original names.
For TypeScript, ensure tsconfig.json has:
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true
}
}
"sourceMap": true generates .js.map files alongside compiled .js files. "inlineSources": true embeds your original TypeScript source in the map, ensuring debuggers can show it even if source files aren't accessible.
For webpack (common in React/Vue apps), ensure your webpack config includes:
module.exports = {
devtool: "source-map", // or 'eval-source-map' for faster rebuilds
// ... rest of config
};
Now when you set a breakpoint in UserProfile.tsx, the debugger pauses thereâeven though the browser is actually executing UserProfile.js. You see TypeScript types in hover tooltips, original variable names, and your source code structure. The transpilation is invisible.
Common source map issue: Breakpoints set but never hit, even though you know the code runs. Check the browser's Developer Tools Network tabâis the .js.map file loading? If not, your web server might not be serving it, or the //# sourceMappingURL= comment at the bottom of your .js file might have the wrong path. Fix the server configuration or webpack's output.sourceMapFilename setting.
React DevTools for component tracing
React's component model makes execution flow less obvious than traditional JavaScript. When does a component re-render? Why did this effect run? What props triggered this update? React DevTools (a browser extension) reveals React's internal execution model.
Install React DevTools for Chrome or Firefox, then open DevTools and find the new "Components" and "Profiler" tabs. These are your windows into React's rendering process.
Components tab: Shows your React component tree. Select any component to see:
-
Props: Current prop values passed to this component
-
State: Internal state (for class components or hooks)
-
Hooks: Full hook state for functional components (useState values, useEffect dependencies, etc.)
-
Rendered by: Which parent component rendered this one
-
Source: Click to jump to the component's source code
Here's where this helps with tracing: You're debugging why a UserDashboard component re-renders 50 times on load. Select it in the Components tab and click the "Rendered by" section. It shows:
App â Layout â Dashboard â UserDashboard
Now select Dashboard and examine its hooks. You see a useState hook with state that's updating rapidly. Check its valueâit's an object that's being recreated every render:
const [config, setConfig] = useState({ theme: "dark", layout: "grid" });
Each render creates a new object identity, which React treats as a new value, triggering re-renders of children. You discovered this by tracing the component tree and inspecting state, not by reading code and trying to mentally model React's diffing algorithm.
Profiler tab: Records component rendering performance. Click "Record," perform your action, then stop. You get a flame graph showing which components rendered, how long each took, and why they rendered. Hover over UserDashboard and it shows: "Rendered: 50 times. Reason: Props changed." Click to see which props changed each time. This is execution tracing specifically for React's rendering model.
Debugging webpack/bundler transformations
Modern JavaScript projects use bundlers (webpack, Vite, Parcel, esbuild) that transform your code: splitting into chunks, tree-shaking unused code, injecting environment variables, replacing imports, and more. Sometimes you need to debug what the bundler actually produced, not your source code.
Scenario: Your app works in development but breaks in production. You suspect a bundler optimization is the issue. You need to trace production code.
First, create a production build with source maps:
# For webpack-based apps (Create React App, Vue CLI)
npm run build
# Ensure your webpack config has devtool set for production
# In webpack.config.js or next.config.js:
devtool: 'source-map' // Even in production
Serve the production build locally:
npx serve -s build
Open Chrome DevTools, go to Sources, and find your bundled JavaScript. With source maps, you'll see your original source files. Set breakpoints, reload, and debug as normal.
But what if you need to see what the bundler actually generated? Disable source maps temporarily by commenting out the //# sourceMappingURL= line at the bottom of your bundled .js file, or by unchecking "Enable JavaScript source maps" in Chrome DevTools settings. Now you see the raw bundled output.
This reveals bundler-specific issues:
-
Dead code elimination: Did tree-shaking remove code you needed?
-
Environment variable substitution: Is
process.env.API_URLcorrectly replaced? -
Dynamic imports: Are code splits working as expected?
-
Polyfills: Are polyfills being included correctly?
You trace through the bundled code to see exactly what ships to the browser, not what you wrote. This is particularly valuable when debugging third-party library integration issuesâsometimes the library works in isolation but breaks when bundled with your app due to module resolution issues or global variable conflicts.