This article (initially published on Rails Designer) was taken, but adapted for the web, from the book JavaScript for Rails Developers that was recently released.
In this article I am going beyond console/log
, like various other levels (warn and error), console.trace
and console.group
. What? 🤯
You most likely know console.log already. console is a global variable, and all global variables are properties of the window object. This means window.console.log and console.log are effectively the same thing. It is still a great debugging tool today, but it has quite a few more tricks up its sleeve than just printing some text.
Before exploring those, let's add a little helper to make us, lazy developers, a little bit more productive. Head over to your app/javascript/application.js:
// app/javascript/application.js
// …
window.log = (...params) => console.log(...params)
Now you can write: log("Hello from Rails Designer!")
instead. 7 characters saved × 0,24 seconds per character × 15 logs = 25,2 seconds saved per debugging session! 🤑
You are not limited to just text or single variables. Multiple arguments can be passed. And also string substitution is supported.
log("User:", { name: "Kendall", age: 30 })
log("Status:", "active", "since:", new Date())
log("%s will be the %s book to finally grasp JavaScript", "“JavaScript for Rails Devs”", "best")
Console logging levels
There are many other logging levels available, just like with ActiveSupport::Logger.
console.info("Info message") // for important information, usually positive/success flows
console.warn("Warning message") // issues, deprecated features, things to watch out for
console.error("Error message") // actual errors, exceptions, critical issues
console.debug("Debug message") // info for debugging, often filtered out in production
Each level has different colors and icons in the developer tools. You can also filter or search by log level and configure which ones to show or hide. You can choose to hide certain levels, like debug. Each logging level supports the same attributes as seen for console.og()
.
Trace
console.trace
outputs the current stack trace to the console, which will help you track the execution path and function call hierarchy that led to a specific point in your code. Adding it the top of Stimulus controller's connect method outputs:
console.trace() editor_controller.js:54:12
connect editor_controller.js:54
connect stimulus.js:1498
connectContextForScope stimulus.js:1668
scopeConnected stimulus.js:2054
elementMatchedValue stimulus.js:1961
tokenMatched stimulus.js:999
// …
This trace shows Stimulus' controller lifecycle, where the connect() hook in the editor_controller.js on line 54 was called as part of Stimulus's initialization sequence. It then first matches the DOM element with its controller through tokenMatched, sets up the scope (scopeConnected), and finally executes the controller's connect() method.
This can help you find out which function led to a particular method being called, identify unexpected function calls or debug race conditions.
Assert
console.assert(false, "NUCLEAR LAUNCH DETECTED")
is a debugging method that tests if a condition is, in this case, false. Good for deliberately triggering an error during development or for adding impossible state checks in code (like below example). You can pass it any assertion:
function gettingGood(level) {
console.assert(level >= 0 && level <= 100,
"Getting good at JavaScript can only be between 0 - 100!"
)
// getting good logic here…
}
Table
The console.table()
method displays array or object data in a formatted table in your browser's console. Useful at times to read, analyze or compare. Could be used to list performance monitoring data, API responses or configuration between development and production.
const users = [
{ id: 1, name: "Kendall", role: "admin", lastLogin: "1995-12-21" },
{ id: 2, name: "Cam", role: "user", lastLogin: "2004-07-01" }
]
console.table(users)
Dir
With console.dir()
you can display a list of properties for a specified object with customizable output formatting. It is not something you are likely to use much, but it is there if you need it.
console.dir(document.body)
console.dir(document.querySelector("[data-controller='editor']"))
console.dir(document.forms[0])
console.dir(this.element) // when inside a Stimulus controller
Group
Console groups create collapsible sections in your console, which helps organize related log messages into nested, indented blocks. Use it like so:
processOrder() {
console.group("Checkout Process")
console.log("1. Validating cart…")
this.validateCart()
console.log("2. Processing payment…")
this.processPayment()
console.log("3. Creating order…")
this.createOrder()
console.groupEnd()
}
Time
console.time()
wraps around logic and can measure execution time (in milliseconds) between start and end points. The labels for time and timeEnd need to be exactly the same.
console.time("Controller setup")
this.editor = new EditorView({
doc: this.contentValue,
parent: this.element,
extensions: this.#extensions
})
console.timeEnd("Controller setup")
Then there is also console.timeLog("Controller setup", "some custom value")
. It allows you to see intermediate timing measurements without stopping the timer.
connect() {
console.time("Controller setup")
this.collaboration = new Collaboration(this.contributorValue.id)
console.timeLog("Controller setup", "Before EditorView instantiation")
this.editor = new EditorView({
doc: this.contentValue,
parent: this.element,
extensions: this.#extensions
})
console.timeLog("Controller setup", "After EditorView instantiation")
this.collaboration.attach({ to: this.editor })
console.timeEnd("Controller setup")
}
Count
console.count()
helps debug code by counting how many times a specific operation occurs (using a required string label as the identifier). Then use console.countReset()
to restart that count from zero using the same label. Useful for tracking API calls, retries, or any repeating operation you want to monitor.
class Stripe {
async fetch() {
console.count("stripe-api")
try {
const response = await fetch("https://api.stripe.com/v1/customers")
console.countReset("stripe-api")
return response.json()
} catch (error) {
if (this.retries < 3) {
await this.delay(1000)
return this.fetch()
}
throw error
}
}
}
And now for the final act: use them altogether! 🎻
async function confirmPaymentIntent(clientSecret) {
console.group("Stripe Payment")
console.time("request")
console.log("Intent:", { clientSecret: `${clientSecret.slice(0, 10)}…` })
// Simulate Stripe API call
await new Promise(resolve => setTimeout(resolve, 300))
console.timeLog("request", "After promise")
console.table([
{ status: "processing", time: new Date().toLocaleTimeString() }
])
console.timeEnd("request")
console.groupEnd()
return await stripe.confirmPayment({ clientSecret })
}
Then when done, or you feel like spring cleaning, console.clear()
method clears the console output in your browser's developer tools. But you can also use the keyboard shortcut (typically CMD+K
for most browsers on macOS).
console.clear()
So, tell me, which of the above utilities have you learned about today? 😊 And how many are you actually going to use? 😬
Don't forget to subscribe! This article is just a taste of what's in JavaScript for Rails Developers (launching next week), and newsletter subscribers might find a special discount code in their inbox… 🤫