All Node.js applications use some level of logging to communicate program progress. However, we rarely see any logging in frontend code. This is primarily because:
- Frontend developers already get a lot of feedback through the UI.
-
console
object has a bad history of cross-browser compatibility (e.g. in IE8 console object was only available when the DevTools panel was open. Needless to say – this caused a lot of confusion.)
Therefore, it didn’t surprise me when a frontend developer asked me how are we going to log errors in our React project:
Should logs be freely used everywhere and leave it up to the bundler to handle removal of those? To reduce the size footprint perhaps? I read that some older browsers do not have console defined. So it’s advisable to remove them or handle its presence.
Writing a Logger
The first thing to know is that you mustn’t use console.log
directly. Lack of a console standard aside (there is a living draft), using console.log
restricts you from pre-processing and aggregating logs, i.e. everything that you log goes straight to console.log
.
You want to have control over what gets logged and when it gets logged because once the logs are in your browser’s devtools, your capability to filter and format logs is limited to the toolset provided by the browser. Furthermore, logging does come at a performance cost. In short, you need an abstraction that enables you to establish conventions and control logs. That abstraction can be as simple as:
const MyLogger = (...args) => {
console.log(...args);
};
You would pass-around and use MyLogger
function everywhere in your application.
Enforcing what gets logged
Having this abstraction already allows you to control exactly what/ when gets logged, e.g. you may want to enforce that all log messages must describe namespace and log severity:
type LogLevelType =
'debug' |
'error' |
'info' |
'log' |
'trace' |
'warn';
const MyLogger = (namespace: string, logLevel: LogLevelType, ...args) => {
console[logLevel](namespace + ':', ...args);
};
Our application is built using many modules. I use namespace to identify which module is producing logs, as well as to separate different domain logs (e.g. "authentication", "graphql", "routing"). Meanwhile, log level allows to toggle log visibility in devtools.
Filtering logs using JavaScript function
You may even opt-in to disable all logs by default and print them only when a specific global function is present, e.g.
type LogLevelType =
'debug' |
'error' |
'info' |
'log' |
'trace' |
'warn';
const Logger = (logLevel: LogLevelType, ...args) => {
if (globalThis.myLoggerWriteLog) {
globalThis.myLoggerWriteLog(logLevel, ...args);
}
};
The advantage of this pattern is that nothing gets written by default to console (no performance cost; no unnecessary noise), but you can inject custom logic for filtering/ printing logs at a runtime, i.e., you can access your minimized production site, open devtools and inject custom to log writer to access logs.
globalThis.myLoggerWriteLog = (logLevel, ...args) => {
console[logLevel](...args);
};
Sum up
If these 3 features are implemented (enforcing logging namespace, log level and functional filtering of logs) then you are already up to a good start.
- Log statements are not going to measurably affect the bundle size.
- It is true that console object has not been standardised to this day. However, all current JavaScript environments implement console.log. console.log is enough for all in-browser logging.
- We must log all events that describe important application state changes, e.g. API error.
- Log volume is irrelevant*.
- Logs must be namespaced and have an assigned severity level (e.g. trace, debug, info, warn, error, fatal).
- Logs must be serializable.
- Logs must be available in production.
I mentioned that log volume is irrelevant (with an asterisk). How much you log is indeed irrelevant (calling a mock function does not have a measurable cost). However, how much gets printed and stored has a very real performance cost and processing/ storage cost. This is true for frontend and for backend programs. Having such an abstraction enables you to selectively filter, buffer and record a relevant subset of logs.
At the end of the day, however you implement your logger, having some abstraction is going to be better than using console.log
directly. My advice is to restrict Logger interface to as little as what makes it useable: smaller interface means consistent use of the API and enables smarter transformations, e.g. all my loggers (implemented using Roarr) require log level, a single text message, and a single, serializable object describing all supporting variables.
Top comments (1)
This is a good intro to console logging. For visual learners(like me), I recommend watching Advanced Logging with the JavaScript Console on
egghead
.It requires a subscription for
egghead
, but totally worth over time.