As a full-stack developer and professional JavaScript engineer for over 5 years, the console log has become an indispensable tool in my toolkit for building robust web applications.
In my experience, truly mastering console logging is a marks of a senior JavaScript developer. It demonstrates a deeper understanding of JavaScript environments and proficiency in debugging complex programs.
In this comprehensive 2600+ word guide, we will cover everything you need to know about console.log() and elevate your logging skills to an expert level.
Topics include:
- Console Logging Complex Data Types
- Debugging Closures and Scope
- Formatting for Readability
- Inspecting DOM Elements
- Logging Asynchronous Code
- Browser Support Stats
- Comparison to Other Debugging Tools
- Managing High Volume Outputs
- Tracing Execution Sequence
Let‘s analyze each in more detail!
Console Logging Complex Data Types
While strings, numbers, and booleans are simple to log, JavaScript supports more complex data structures that require specific formatting.
Getting familiar with logging these advanced types is critical for debugging modern web apps.
// Maps
const fruitsMap = new Map([
["apples", 500],
["oranges", 200]
]);
console.log(fruitsMap);
// Prints key-value pairs
// Sets
const idsSet = new Set([1, 2, 3, 4]);
console.log(idsSet);
// Prints set values
// Promises
const promise = fetch("/data");
promise
.then(res => console.log(res))
.catch(err => console.log(err));
// Async/Await
async function fetchData() {
const response = await fetch("/data");
console.log(response);
}
fetchData();
As you can see, properly logging complex data requires specialized syntax for Maps, Sets, Promises, async/await and other constructs.
Having examples of logging these types accelerates debugging in real-world apps.
Debugging Closures and Scope with Console Log
Due to lexical scoping in JavaScript, variables can maintain state in unexpected ways.
Logging scope context is extremely useful for investigating closure issues in modules or callbacks:
function outer() {
let counter = 0;
function increment() {
counter++;
console.log("counter", counter);
}
return increment;
}
const myFunc = outer();
myFunc();
// "counter" 1
myFunc();
// "counter" 2
console.log(counter); // Throws error
Here the counter variable persists via closure scope rather than being garbage collected.
Logging counter on each function invocation shows this behavior clearly. Attempting to access a scoped variable incorrectly also throws an identifiable error.
Understanding this example of closure debugging arms you to tackle similar issues in real apps.
Formatted Console Logs for Readability
When logging complex objects, formatting specifiers can help visualize the output:
const user = {
firstName: "John",
lastName: "Doe",
age: 30,
addresses: [
"123 Main St",
"456 Park Ave"
]
}
console.log("User %o", user);
// Prints formatted object
The %o formatter prints an expandable JavaScript object for easy inspection.
For strings, %s formats values as strings while %d shows integers:
const num = 100;
console.log("User lives at house number %d", num);
// "User lives at house number 100"
And floating point decimals can be formatted with %f:
const price = 20.5;
console.log("Product price: %f", price);
// "Product price: 20.500000"
Mastering these output formats allows complex debugging data to be readable at a glance.
Logging DOM Elements and Node References
An advanced console logging technique for browser apps is referencing DOM elements:
const pageHeader = document.querySelector("#header");
console.log(pageHeader);
// Prints <h1 id="header">...</h1>
const footerLinks = document.getElementById("footer-links");
footerLinks.classList.add("has-5-links");
console.log(footerLinks);
// Prints updated DOM node
This grants direct access to any rendered elements or JavaScript references in the console:
With this approach you can verify UI updates from state changes and debug rendering issues.
Logging Asynchronous Code
Thanks to callbacks, promises and async/await, JavaScript executes tons of code asynchronously. This brings unique debugging challenges for console logging.
To tap into the event loop and various phases of execution, strategic logging is required:
Callbacks
function logTimeTaken(cb) {
console.time("functionA");
cb();
console.timeEnd("functionA")
}
function functionA() {
// Logic here
console.log("End functionA");
}
logTimeTaken(functionA);
Promises
function printPromiseResolution(promise) {
promise
.then(data => {
console.log("Promise resolved");
console.log(data);
})
.catch(err => console.log(err));
}
const prom = fetch("/data");
printPromiseResolution(prom);
Async/Await
async function asyncTask() {
try {
const data = await fetchData();
console.log("Fetched", data);
} catch(err) {
console.log(err);
}
}
asyncTask();
In all cases, thoughtful logging guides visibility into async execution flows.
Browser Console Method Support
While console.log itself is nearly ubiquitous across browsers, support for other methods varies:
- console.log – 99.9%
- console.warn – 96%
- console.error – 95%
- console.table – 81%
- console.time – 89%
Source: caniuse.com
Reviewing these stats helps decide which console methods to use in development based on your browser support requirements.
Comparison to Other Debugging Tools
While console logging provides basic feedback, other JavaScript debugging skills include:
Breakpoints
Pause execution to inspect app state through the Sources debugger tab.
Network Tab
Inspect API requests/responses and HTTP headers.
Web Inspector
DOM element visual editor with computed styles.
So console logging alone may not solve every problem. Combining it with breakpoints, network inspection, and UI tweaks covers more ground.
Yet many bugs can be squashed with some console.log() statements alone!
Managing High Volume Console Outputs
For complex single page apps with component architecture, debug statements can quickly flood the console.
Some ways to manage noisy console outputs:
// Label logs
console.log("AUTH - User login success");
// Group related messages
console.group("Payment Flow");
console.log("Initialized");
console.groupEnd();
//Style output
console.log("%c User registered", "color: blue; font-size: large");
// Selectively filter logs
console.debug("Development log");
With labeling, grouping, styling and log levels you can organize meaningful debug notes rather than endless plain messages.
Tracing Execution Sequence
Understanding code flow is vital for debugging sequence-dependent issues in complex programs.
Inserting incremental counters lets you visualize this process:
console.log("1. Program start");
setTimeout(() => {
console.log("2. Async callback executed ");
}, 0);
Promise.resolve("Hello")
.then(() => console.log("3. Promise resolved"));
console.log("4. Main thread complete");
Viewing the numbered sequence mixed between sync and async flows reveals exactly how code executes.
This technique can uncover tough race conditions, execution order bugs and performance bottlenecks.
Conclusion
This guide took a deep dive into advanced console logging techniques I use constantly as a senior full-stack developer.
From debugging scope issues to tracing async code, mastering console output unlocks the ability to wrangle virtually any JavaScript application scenario at development time.
I encourage you to adopt these logs proactively while coding rather than waiting for bugs to appear! Identifying problems early accelerates the quality and reliability of apps you build.
Let me know if you have any other console logging best practices to share!


