I consider myself a backend software engineer - and as any backend engineer would attest, a large part of our life is spent monitoring, troubleshooting, and debugging our applications. The fundamental rule of software development is that software will fail - what separates new developers from experienced ones is how they plan for those failures. Robust and effective logging is an important part of planning for failure, and eventual mitigation. As it is for backend development, logging can be useful for frontend software development, but it goes much further than just troubleshooting and debugging. Effective frontend logging can also make the development experience productive, fast, and fun.
While I’m a big proponent and diligent practitioner of Test-driven development, I love the flexibility, the richness of information, and code confidence browsers provide to frontend developers who make effective use of console.log()
. I thought I’d share some frontend logging tips and tricks I’ve learned and incorporated in my workflow over time while building Firecode.io - in the hope that some of these will help you make your development workflow a bit more productive and fun!
I like to divide these tips into two broad categories - quick n’ dirty logging for when you’re actively building and debugging your application, and durable production logging - to know when your app’s working as expected and when it’s not.
Tips for quick n’ dirty development logging with console.log()
Don’t use console.log().
Yes, that’s right. I don’t use console.log()
. Well, ok I write wrappers that use console.log()
(more on that in the production logging section), but if you want to log something in your app to see what’s going on, use console.trace()
instead. In addition to giving you everything console.log()
does, it also outputs the entire stack trace so you know where exactly the message is emitted from.
Use ES6’s computed property names to identify your objects and avoid variable name confusion
This one is straightforward - use ES6’s computed property syntax and wrap the objects you wish to log in curly braces inside console.log()
- i.e. use console.log({user})
vs console.log(user)
. You’ll find them neatly logged with the variable name set as the key, and the value as the object itself. This is especially useful when you’re in a hurry and want to log multiple objects in the same console.log()
command.
Embrace tiered log levels - error, warn, info
console.log(param)
by default logs at the INFO
level - however, you also have 3 other logging levels at your disposal which you should make use of - console.debug()
, console.warn()
and console.error()
. Besides formatting differences (notice the different colors?), the browser’s developer console also lets you easily filter out logs at different levels with a convenient dropdown to declutter your logs.
When logging lists of items, use console.table()
This one is self-explanatory and one of my favorite console functions - if you ever need to log a list of objects, give console.table()
a try.
Quickly debug with debugger
Want to save a precious few seconds? Instead of finding your file in the developer console to add a breakpoint, drop a debugger
in your code to halt execution when the line is executed. From this point on, you can debug and step over / into functions as you normally would.
Granular performance profiling with console.profile()
and console.time()
Want to profile an exact user flow in your application to find hot spots? Trigger console.profile(profileName)
at the start of the action, and console.profileEnd(profileName)
at the end to inspect the CPU profile for the flow.
Related, you can measure exactly how long a flow takes with triggering console.time(id)
at the start of the flow, and console.timeEnd(id)
at the end.
Count labelled executions with console.count()
This one’s one of those console functions I haven’t found much use for personally, but it’s there if you need it. console.count(label)
can help you know exactly how many times a piece of code gets executed - which could be useful for finding and eliminating race conditions and other scenarios.
Prettify your logging with CSS
This is by far my favorite console feature and one I make extensive use of in production logging (more on this in the production logging section). You can make use of format strings to format your log messages. The %c
is the placeholder for CSS styles, and anything after is your message.
You can also style multiple elements by extending your format string to include %s
for string parameters.
Since I’m a highly visual person, I like to spend some time making my info and debug logs look pretty and be useful at the same time. I make extensive use of this feature for production logging in Firecode.io - which is an excellent segue for the next section.
Production logging with console.log()
Getting frontend code production ready involves a number of steps - some being uglifying and compressing your code, generating cacheable asset digests, and removing console.log()
s from your app. Why? Because you don’t want your users to have to open the developer console to interact with your app, which nullifies the usefulness of your logs and leaves them as pure security holes for the more inquisitive to take advantage of. At the same time, when you use your own app, you most likely want the most granular level of logging to understand how your app is functioning and find and squash bugs. If your app is being used by others, you’d also want to be notified when your application’s users encounter errors so you can track down and fix your code. Here’s a couple of things I do to satisfy these requirements as best one could on the frontend:
Don’t use console.log()
Instead, write a wrapper class that includes logic for conditionally logging based on the log level based set on a global variable by the backend. Warning - you’ll see TypeScript code snippets ahead - if you’re not familiar with TypeScript, think of it as a superset of JavaScript with types tacked on (gross over-simplification) - i.e. const str = “some string”;
becomes const str: string = “some string”
- types are added after a variable followed by a semicolon.
In the case of Firecode.io, I wrote my own frontend framework that utilizes RxJS, but includes familiar concepts such as components from other popular frameworks such as React and Vue - while adding additional concepts such as engines for processor-heavy code blocks, channels for WebSocket messages, and clients for HTTP requests. Visualizing all these pieces working together was critical, so I implemented custom formatting in a Logger
wrapper class that formats and visually differentiates logs from each part of the application.
Instead of calling console.log("Cache SET to", {value})
, I call Logger.debug(“Cache set to”, {value}, Framework.Cache)
. The Logger
class has a TypeScript enum that maps each framework component to the color to be used:
This allows me to visually focus on components of the app during development - for example, if I want to see what the WsRequestCache
is doing I can tune out everything else besides the turquoise badged logs.
Protect your logs by having the backend set your log level
I have Firecode.io configured to turn on debug level logging by default for admin users with a JavaScript variable that is set by the backend. While adventurous users can still find and set these flags in the developer console to turn on granular logging, it is better than having all logs exposed to every user of your application by default, or having a post-processor remove all logging completely from your application in production.
Set in a Ruby on Rails view:
const logLevel: number = <%= @app.get_log_level_for_user %>
And in the Logger class:
class Logger {
...
...
static info(...) {
shouldLog(Level.INFO) && console.log(...);
...
}
}
Log and notify on actionable errors
Last but not least, you want to be notified when exceptional conditions are encountered by users without necessarily outputting logs to the developer console. You can do this by including a call to pipe your errors to a third party APM service such as AppSignal in your Logger‘s error function like so:
class Logger {
...
...
static error(e) {
if (shouldLog(Level.ERROR)) {
console.error(e);
}
appsignal.sendError(e);
}
}
AppSignal includes integrations to pipe your errors to outbound notifications services such as Slack, PagerDuty, and OpsGenie - you can even hook up a project management tool such as JIRA or Trello to automatically create issues and bugs for your team.
Summary
I really hope these tips and anecdotes make your frontend development experience a little more productive and fun! I’ve obviously only touched the surface of logging ninjitsu in this post, so if you have any more tips to share I’d love to read them over on my Twitter.
Two parting plugs - I’m rebuilding Firecode.io from the ground up with a brand new set of coding interview questions for JavaScript, Java, Python, and Scala. If you’re interested in coding interview prep that adapts to your learning style and is fun - sign up with your email here I’ll also be putting out more content about building a production scale web app like Firecode.io from scratch as a side project - follow me at @ackshaey or @firecodeio to learn more. Lastly, if you’re new to JavaScript and want to understand how object-oriented JavaScript and prototypal inheritance work under the hood, check out my favorite book on the subject - The Principles of Object-Oriented JavaScript, and if you’re interested in learning more about why you should use TypeScript instead, check out Effective TypeScript.
Top comments (18)
You wrote: "Don’t use console.log() Instead, write a wrapper class..."
Console.log()/table()/etc... print in console place (filename and string number) where was he called.
joxi.ru/gmv6P5LsqgZKEm?d=1
If wrap console.log() in class in console always printing file and string number where console.log() is live in Log class!!. How can I know without additional params where log() was called?
Good question, which drives home the reason why this tip was listed under the production logging section and not development or debugging. The question you'd need to ask yourself is how useful is it to have that extra file:line link there? If you're doing things right and have compressed / minified your app's assets in production you're likely not going to find the location link useful anyway - instead your log messages should have all the info you need to identify where the log is emitted from and be actionable. Also keep in mind that console.trace() will still have the entire stack trace one level down even when invoked from a wrapper.
Do not write a wrapper around console.log, unless you're using an opinionated framework like React or Angular, which then you can expose the functionality via a custom, injectable service. Otherwise, do use console.log or any language equivalent and stay away from heavy-handed logging frameworks.
Awesome article. I've never even heard about these console methods as .table, .trace, etc. Very informative. Thank you! 🤓
Very useful tips, thanks 👍
Great...
Thank you for sharing :D
Thanks for the tips!
Great article! I will start immediately to use this stuff!
Very useful, never knew you can do so much to improve console logs!
Great article thanks for share 👍