Part 86: Debugging LWC in Salesforce
If you have been following this series, you already know how to build Lightning Web Components, handle events, work with lifecycle hooks, and manage data. In Part 85, we looked at exception handling in LWC, which is about writing code that anticipates and recovers from errors gracefully. This post is the other side of that coin — what do you do when something goes wrong and you need to figure out why?
Debugging is one of those skills that separates developers who build things from developers who build things quickly. You can write perfect code all day, but the moment you are working with a real Salesforce org, real data, and real users, something will break in a way you did not expect. The question is whether you spend five minutes finding the issue or five hours.
Back in Part 42, we covered debugging Apex on the server side using debug logs, the Developer Console, and replay debugger. Debugging LWC is a different experience because your code runs in the browser, not on the Salesforce server. That means your primary tools are the browser’s built-in developer tools — specifically the console and the JavaScript debugger. Salesforce also gives you a dedicated Lightning debugger mode that provides extra visibility into what the framework is doing behind the scenes.
This post covers:
- How to Turn on the Lightning Debugger — Enabling debug mode in your org and what it gives you.
- How to Utilize Console Logging — Going beyond
console.logto use the full suite of console methods for effective debugging. - How to Use the Browser Console to Set Breakpoints and Debug Your Code — Stepping through your LWC code line by line in the browser.
- Section Notes — Key takeaways and common pitfalls.
Let’s get into it.
How to Turn on the Lightning Debugger
Before you start digging into browser dev tools, the first thing you should do is make sure debug mode is enabled in your Salesforce org. By default, Salesforce serves minified and optimized JavaScript to the browser. That means the code you see in the browser console is compressed, variable names are shortened, and stack traces are nearly impossible to read. Debug mode tells Salesforce to serve the unminified, human-readable version instead.
Enabling Debug Mode
To enable debug mode, go to Setup, then search for Debug Mode in the quick find box. You will see a page that lists all users in your org. Find your user (or the user you want to debug for), check the box next to their name, and click Enable. That is it.
Once debug mode is enabled, the next time you load a Lightning page, Salesforce will serve the full, uncompressed JavaScript for the Lightning framework and your components. This makes the code readable in the browser’s Sources panel and gives you meaningful stack traces when errors occur.
What Debug Mode Changes
There are a few important things to understand about what debug mode actually does.
First, it disables the JavaScript minification for Lightning components. The code you see in the browser will closely match what you wrote in your source files. Variable names will be intact, function names will be readable, and you can actually set breakpoints on specific lines of your code.
Second, it enables additional framework-level logging. The Lightning framework will output warnings and informational messages to the browser console that it normally suppresses. These messages can help you catch issues like incorrect property types, missing attributes, or deprecated API usage.
Third, and this is important — debug mode makes your org slower. The unminified JavaScript files are significantly larger, so pages take longer to load. This is why you enable it only for specific users, not for the entire org. Never enable debug mode for all users in a production environment. Use it in your developer org or sandbox, and only enable it for your own user account.
The Lightning Component Debug Tool
In addition to debug mode, Salesforce offers a Chrome extension called the Salesforce Lightning Inspector (though its availability and naming have changed over the years). If it is available for your Salesforce version, this extension adds a panel to Chrome DevTools that shows you the component tree, event flow, and performance metrics for Lightning components on the page.
For LWC specifically, the browser’s native dev tools are your best friend. The Lightning Inspector was originally built for Aura components, and while it can show some LWC information, the native Sources panel and Console in Chrome or Firefox give you everything you need for debugging Lightning Web Components.
How to Utilize Console Logging to Assist in Debugging
The console.log method is the most common debugging tool in JavaScript, and for good reason — it is fast, easy, and gives you immediate feedback. But most developers only use console.log and ignore the rest of the console API. Let’s look at the full toolkit.
console.log — The Basics
You already know this one. Drop a console.log into your code, and whatever you pass to it shows up in the browser console.
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';
export default class AccountList extends LightningElement {
accounts;
@wire(getAccounts)
wiredAccounts({ error, data }) {
if (data) {
console.log('Accounts loaded:', data);
this.accounts = data;
} else if (error) {
console.log('Error loading accounts:', error);
}
}
}
A few tips to make console.log more useful. First, always include a label string before your data. Writing console.log(data) is fine when you only have one log statement, but the moment you have five or six scattered across your component, you will not be able to tell which output belongs to which log call. Always use a prefix like console.log('wiredAccounts data:', data).
Second, you can log multiple values in a single call. console.log('Name:', this.name, 'Id:', this.recordId) prints everything on one line, which is easier to scan than separate log statements.
Third, if you are logging an object, the browser console lets you expand it and inspect its properties interactively. But be careful — the console shows a live reference to the object, not a snapshot. If the object changes after you log it, the console might show the updated values when you expand it later. To avoid this, log a snapshot with console.log('data:', JSON.parse(JSON.stringify(data))).
console.error and console.warn
Use console.error for actual errors and console.warn for things that are not broken but deserve attention.
handleSave() {
if (!this.recordId) {
console.error('Cannot save: recordId is missing');
return;
}
if (!this.hasChanges) {
console.warn('Save called but no changes detected');
}
// proceed with save
}
These methods behave like console.log but with visual differences. console.error shows a red background with an error icon and includes a stack trace. console.warn shows a yellow background with a warning icon. This visual distinction makes it much easier to spot problems when you are scrolling through dozens of console messages.
console.table
When you are working with arrays of objects — which happens constantly in Salesforce development — console.table formats the data as a readable table in the console.
@wire(getAccounts)
wiredAccounts({ error, data }) {
if (data) {
console.table(data.map(acc => ({
Name: acc.Name,
Industry: acc.Industry,
AnnualRevenue: acc.AnnualRevenue
})));
this.accounts = data;
}
}
Instead of expanding nested objects one by one, you get a formatted table with columns and rows. This is invaluable when you are trying to verify that your data looks correct before it hits the template.
console.group and console.groupEnd
When your component has complex logic with multiple steps, grouping your log output makes it far easier to follow.
handleSubmit() {
console.group('handleSubmit');
console.log('Form values:', this.formData);
console.log('Validation result:', this.validate());
console.log('Record ID:', this.recordId);
console.groupEnd();
}
This creates a collapsible group in the console. All the log statements between console.group and console.groupEnd are nested under a single header. You can expand or collapse each group, which is incredibly helpful when you are debugging a flow that involves multiple method calls.
console.time and console.timeEnd
If you suspect a performance issue, console.time and console.timeEnd let you measure how long a block of code takes to execute.
async loadData() {
console.time('loadData');
const result = await getAccounts();
console.timeEnd('loadData');
this.accounts = result;
}
The console will print something like loadData: 342.5ms, giving you a quick measurement without needing to manually calculate timestamps.
Cleaning Up Console Statements
One thing to always keep in mind — remove your console statements before deploying to production. Leftover console.log calls clutter the browser console for anyone who has dev tools open, and they can accidentally expose data that should not be visible. Some teams use ESLint rules to catch stray console statements in their CI pipeline. If your team does not have that set up, make it a habit to search for console. in your code before you push.
How to Use the Browser Console to Set Breakpoints and Debug Your Code
Console logging is great for quick checks, but when you need to understand exactly what your code is doing at a specific moment — what variables contain, what path execution is taking, why a condition is evaluating the way it is — you need breakpoints.
Opening DevTools and Finding Your Code
Open Chrome DevTools with Cmd + Option + I on Mac or Ctrl + Shift + I on Windows. Click the Sources tab. On the left side, you will see a file tree. Your LWC code lives under a path that typically looks like something along the lines of lightning/n/ followed by your namespace and component name. With debug mode enabled, the files will be readable and match your source code closely.
You can also use the keyboard shortcut Cmd + P (or Ctrl + P on Windows) to open the file search, then type your component name. This is usually faster than navigating the file tree, especially in a large org with many components.
Setting Breakpoints
Click on any line number in the Sources panel to set a breakpoint. A blue marker appears on that line. The next time your code executes that line, the browser will pause execution and let you inspect the current state.
When the code is paused at a breakpoint, you can:
- Hover over variables to see their current values in a tooltip.
- Check the Scope panel on the right side to see all local variables, closure variables, and the
thiscontext. - Use the Call Stack panel to see what function called the current function and trace the execution path.
- Use the Watch panel to add specific expressions you want to monitor, like
this.accounts.lengthorthis.template.querySelector('.my-class').
Stepping Through Code
Once you are paused at a breakpoint, the toolbar at the top of the Sources panel gives you four buttons for stepping through your code:
- Resume (play button) — Continue execution until the next breakpoint or the end of the script.
- Step Over (curved arrow) — Execute the current line and move to the next line in the same function. If the current line calls a function, it executes the entire function without stepping into it.
- Step Into (down arrow) — If the current line calls a function, jump into that function and pause on its first line.
- Step Out (up arrow) — Finish executing the current function and pause when it returns to the calling function.
The one you will use most often is Step Over. It lets you walk through your code line by line and watch variables change. Step Into is useful when you want to follow execution into a helper function or an imported utility. Step Out is your escape hatch when you accidentally step into a function you do not care about.
The debugger Statement
If you do not want to manually find your file in the Sources panel, you can add a debugger statement directly in your code.
handleClick(event) {
const selectedId = event.target.dataset.id;
debugger;
const account = this.accounts.find(acc => acc.Id === selectedId);
this.selectedAccount = account;
}
When the browser hits the debugger statement, it pauses execution exactly as if you had set a breakpoint on that line — but only if DevTools is open. If DevTools is closed, the debugger statement is ignored. This is a convenient way to set temporary breakpoints without searching through the Sources panel, but like console.log, remember to remove these before deploying.
Conditional Breakpoints
Right-click on a line number in the Sources panel and select Add Conditional Breakpoint. You can enter a JavaScript expression, and the browser will only pause on that line when the expression evaluates to true.
This is extremely useful in loops or event handlers. For example, if you have a list of 200 accounts and you only want to pause when a specific account is being processed, you can set a conditional breakpoint like acc.Name === 'Acme Corp'. The browser will fly through the other 199 iterations and only stop on the one you care about.
Debugging Wire Methods and Lifecycle Hooks
Wire methods and lifecycle hooks can be tricky to debug because you do not call them directly — the framework calls them. The approach is the same though. Set a breakpoint inside the method, then trigger the condition that causes the framework to invoke it.
For a wire method, the breakpoint will hit when Salesforce returns data (or an error) from the wire adapter. If the wire is not firing at all, check that the reactive property driving it has a value. A common mistake is expecting a wire method to fire before connectedCallback has set up the necessary parameters.
For renderedCallback, remember that it fires every time the component re-renders. If you set a breakpoint there, it might hit many times. Use a conditional breakpoint or a flag variable to narrow it down.
renderedCallback() {
if (this._debugOnce) return;
this._debugOnce = true;
debugger;
// inspect the rendered DOM here
}
Debugging Common LWC Issues
Here are some patterns that come up frequently when debugging LWC.
The template is not updating. Check that the property driving the template is reactive. Properties decorated with @api or @track are reactive. Plain class properties are reactive for primitive values but not for mutations to objects or arrays. If you are pushing items into an array, the template will not notice. You need to reassign the array: this.items = [...this.items, newItem].
An event is not being handled. Verify the event name in the child’s this.dispatchEvent(new CustomEvent('myevent')) matches the handler attribute in the parent’s template: onmyevent={handleMyEvent}. Remember that LWC event names are all lowercase with no hyphens when used in template attributes.
Wire data is not loading. Set a breakpoint in the wire method and check if it fires at all. If it does not, the reactive parameters driving the wire might not have values yet. If it fires but data is undefined and error has a value, inspect the error. Common causes are insufficient field-level security, missing object permissions, or an Apex method throwing an exception.
The component is rendering but looks wrong. Use the Elements panel in DevTools to inspect the rendered DOM. LWC uses Shadow DOM by default, so your component’s internal elements are inside a shadow root. In the Elements panel, expand the shadow root to see your template’s rendered output. Check that CSS classes are applied correctly and that conditional rendering (if:true / lwc:if) is evaluating the way you expect.
Section Notes
Debugging LWC comes down to three layers. First, enable debug mode so you can actually read your code in the browser. Second, use console methods strategically — not just console.log, but console.error, console.table, console.group, and console.time — to get structured visibility into what your code is doing. Third, use breakpoints to pause execution and inspect state when you need to understand exactly why something is happening.
The browser’s DevTools are more powerful than most developers realize. Conditional breakpoints, the Watch panel, and the Call Stack panel can save you hours of guessing. The debugger statement is a quick way to jump straight to the code you care about without navigating the file tree.
A few habits that will serve you well. Always label your console output. Log snapshots of objects instead of live references when timing matters. Use console.group to organize related logs. Set conditional breakpoints instead of adding if statements around your debugger calls. And always clean up your debugging code before it reaches production.
Combined with the exception handling patterns from Part 85 and the Apex debugging skills from Part 42, you now have a solid debugging workflow that covers both the client side and the server side of Salesforce development. In the next section, we will continue building on the LWC topic with more advanced patterns. See you there.