Logging Errors in Client-Side Applications

Share this article

Detective makes notes while standing over a dead body surrounded by potential murder weapons

This article was peer reviewed by Panayiotis «pvgr» Velisarakos, James Wright and Stephan Max. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Detective makes notes while standing over a dead body, surrounded by potential murder weapons

Key Takeaways

  • Utilize the console for initial debugging: The console remains a primary tool for logging errors and messages during development, with methods like `console.log()`, `console.error()`, and others providing different levels of output detail.
  • Enhance console capabilities with libraries: Tools such as Logdown and console.message can supercharge the console’s functionality, allowing for more structured and informative logging, including markdown support and interactive outputs.
  • Implement global error handling: Using `window.onerror` for capturing unhandled exceptions is a basic yet crucial strategy for logging errors that can help in identifying issues that occur during runtime.
  • Integrate stack traces for detailed error context: Libraries like TraceKit and stacktrace.js are instrumental in providing stack traces that can be sent along with error reports to give more context about the error’s origin.
  • Consider server-side logging for comprehensive monitoring: Logging client-side errors to a server allows for remote error monitoring, aggregation of logs from various sessions, and the use of more sophisticated analysis tools.
  • Batch logging to optimize performance: Techniques such as storing log entries in `localStorage` and sending them in batches can help in managing performance impacts, especially for high-volume applications.

Table of Contents

Logging is an important part of any software application, both during active development and when it’s running in production mode.

When you’re working on the server, there are hundreds of libraries available to you regardless of your server-side language of choice, a wide range of storage mechanisms, and all sorts of tools you can use to work with the resulting logs.

However, when it comes to client-side applications, logging is something that often gets overlooked, and the options open to you are rather more limited.

In this article I’ll look at some of the ways in which you can implement logging in a client-side application; particularly in a JavaScript-heavy, single-page application (SPA).

The Console

Perhaps the most common and obvious way to log errors and messages is the console. While it might appear a primitive solution, there’s absolutely no doubt that it’s an invaluable tool for debugging during development, so it’s probably a good place to start.

The implementation of console isn’t always consistent — particularly in IE, perhaps unsurprisingly — but in general there are four key methods available to you:

console.log()
console.info()
console.warn()
console.error()

The output from each of these four methods is subtly different, and most web console implementations (i.e., Dev Tools) allow you to filter messages based on the method used; that is, the logging level.

In order to mitigate the differences between browsers, you can use a wrapper function — such as this one from Paul Irish. The WHATWG is attempting to standardize the console API, but the spec is still at an early stage and unlikely to be implemented for some time.

Tip: If you find that your code is littered with console.log() statements, you might find tools such as grunt-remove-logging or grunt-strip for Grunt, or gulp-strip-debug for Gulp useful for when you move an application into production.

Enhancing the console

There are a couple of libraries you can use to “super-charge” the console.

Logdown

Logdown is a tiny library which provides a few enhancements to the console. You’ll find a demo here.

Logdown allows you to specify prefixes upon instantiation; one possible use for this is to separate out your log messages by module, for example:

var uiLogger = new Logdown({prefix: 'MyApp:UI'});
var networkServiceLogger = new Logdown({prefix: 'MyApp:Network'});

You can then enable or disable the loggers by their prefix, for example:

Logdown.disable('MyApp:UI');
Logdown.enable('MyApp:Network');
Logdown.disable('MyApp:*'); // wildcards are supported, too

Disabling a logger effectively silences it.

Once you’ve instatatied one or more loggers, you can log messages using the log(), warn(), info() and error() methods:

var logger = new Logdown();
logger.log('Page changed');
logger.warn('XYZ has been deprecated in favour of 123');
logger.info('Informational message here');
logger.error('Server API not available!');

Logdown also provides Markdown support:

var logger = new Logdown({markdown: true}); // Technically "markdown: true" isn't required; it's enabled by default
logger.warn('_XYZ_ has been *deprecated* in favour of _123_');

console.message

console.message is another library for beautifying the console’s output.

Here’s a quick animation from the documentation, that shows off some of its features:

console.message in action

Essentially the library provides a chainable interface with methods which allow you to format text, group messages together and make them collapsible, send interactive DOM elements or objects to the log — and even include images.

Limitations of the console

The console is great while you’re building an application and you can have it open in front of you, but unless you happen to be looking over a user’s shoulders, and they happen to have the web console open in their browser, you won’t get to see the result.

What we can do instead is send any errors — or even debug messages during development — to a server somewhere, so that we can access them remotely.

Other Things to Consider

Now that we’ve looked at some of the solutions available to you, let’s look at a few additional considerations.

Capturing global errors

At the very least, it’s worth capturing and logging any unhandled exceptions. You can do this using window.onerror. Here’s a really simple example:

window.onerror = function(message, file, line) {
  console.log('An error occured at line ' + line + ' of ' + file + ': ' + message);
};

Stack traces

Stack traces provide an additional level of detail when an error occurs, which you may wish to make use of in development. There are a couple of libraries that help to build them.

TraceKit

TraceKit allows you to inject stack traces into exceptions, and do something with them (e.g. send them to your server-side logging component) by subscribing to them.

Here’s what the code might look like:

TraceKit.report.subscribe(function yourLogger(errorReport) {
  //send via ajax to server, or use console.error in development
  //to get you started see: https://gist.github.com/4491219
});

Then, in your application:

try {
  /*
   * your application code here
   *
   */
  throw new Error('oops');
} catch (e) {
  TraceKit.report(e); //error with stack trace gets normalized and sent to subscriber
}

stacktrace.js

stacktrace.js is, to quote the documentation , “[a] framework-agnostic, micro-library for getting stack traces in all web browsers”.

It provides a method named printStackTrace() which you can use in an error handler to add a stack trace to your logging function. For example, we could enhance our server-side logger as follows:

function log(data, level) {
  $.post(
    'https://your-app.com/api/logger',
    {
      context     :   navigator.userAgent,
      level       :   level || 'error',
      data         :   data,
      stack_trace :    printStackTrace()
    }
  );
}

Logging Client-Side Errors to the Server

Sending log entries to the server has a number of advantages:

  1. You can capture log entries from your application without being physically at the computer (perfect in production)
  2. You can manage your server-side and client-side logs in the same place, potentially using the same tools
  3. You can set up alerts (e.g. a Slack notification or SMS if a critical error occurs)
  4. Where the console isn’t available or is difficult to view (e.g. when using a mobile’s web view) it’s easier to see what’s going on

Let’s look at a few approaches to this.

Rolling your own server-side logger

In some cases, the simplest solution might be to roll your own server-side logging mechanism.

Here’s an extremely minimal example of the client part using jQuery:

function log(data, level) {
  $.post(
    'https://your-app.com/api/logger',
    {
      context   :   navigator.userAgent,
      level     :   level || 'error',
      data       :   data
    }
  );
}

Some usage examples:

try {
  // some function
} catch (e) {
  log({
    error : e.message
  });
}
log('Informational message here', 'info');

With that in mind, here’s a very basic server-side component to accompany this example, built using Node.js with Express, along with the excellent Winston logging library:

/**
 * Load the dependencies
 */
var express = require( 'express' );
var bodyParser = require('body-parser');
var winston = require( 'winston' );

/**
 * Create the Express app
 */
var app = express();

app.use(bodyParser.urlencoded({ extended: true }));

/**
 * Instantiate the logger
 */
var logger = new ( winston.Logger )({
  transports: [
    new ( winston.transports.Console )(
      { 
        level: 'error'
      }
    ),
    new ( winston.transports.DailyRotateFile )(
      { 
        filename: 'logs/client.log',
        datePattern: '.yyyy-MM-dd'
      }
    )
  ]
});

app.post ('/api/logger', function( req, res, next ) {

  logger.log(
    req.body.level || 'error',
    'Client: ' + req.body.data
  );

  return res.send( 'OK' );

});

var server = app.listen( 8080, function() {
  console.log( 'Listening on port %d', server.address().port );
});

In practice, there are some fundamental limitations to this over-simplified logger:

  1. Most logging mechanisms allow you to configure a minimum logging level so that you can filter out certain entries
  2. It will send log entries immediately, which could lead to your server-side component becoming overloaded

A better way to deal with the second issue is to buffer log entries and send them in batches. A common approach is to use localStorage to store log entries, then send them at particular intervals — be that time-based, when a certain threshold in the number of pending entries is reached, or when the user closes the window or navigates away from your application by utilizing the window.onbeforeunload event.

To get around these issues, let’s look at a ready-made solution for logging from JS apps.

log4javascript

log4javascript is based on the ubiquitous log4j, a Java logging framework which has also been ported to PHP, so if you’re coming from a server-side background you may already have some familiarity with it.

log4javascript uses the concept of appenders, which determine what happens when you call one of its logging methods. The default, PopUpAppender, is arguably rather redundant when you have the dev tools provided by most modern browsers.

What’s probably more useful is the AjaxAppender, which you can use to send log entries back to the server. You can configure the AjaxAppender to send entries in batches at timed intervals using setTimed(), of a certain number using setBatchSize() or when the window is unloaded using setSendAllOnUnload().

log4javascript is available to download from Sourceforge, or the similar Log4js is available on Github. You can refer to the Quickstart to get up-and-running fast.

Here’s an example:

var log = log4javascript.getLogger();
var ajaxAppender = new log4javascript.AjaxAppender('http://example.com/api/logger');
ajaxAppender.setThreshold(log4javascript.Level.ERROR);
ajaxAppender.setBatchSize(10); // send in batches of 10
ajaxAppender.setSendAllOnUnload(); // send all remaining messages on window.beforeunload()
log.addAppender(ajaxAppender);

Alternatively, to send messages at a specific interval:

ajaxAppender.setTimed(true);
ajaxAppender.setTimerInterval(10000); // send every 10 seconds (unit is milliseconds)

Other libraries

If your project uses jQuery, you might want to look into jquery logger which allows you to log via Ajax; however, it doesn’t support batches. It does integrate nicely with Airbrake as a back-end, though.

loglevel is a lightweight and extensible JS-based logging framework, which supports Ajax via the separate serverSend plugin.

Roll Your Own Batch-Compatible Logger

Here’s a simple proof-of-concept of a logger which sends messages in batches. It’s written using vanilla JavaScript with ES6 features.

"use strict";
class Logger {

  // Log levels as per https://tools.ietf.org/html/rfc5424
  static get ERROR()  { return 3; }
  static get WARN()   { return 4; }
  static get INFO()   { return 6; }
  static get DEBUG()  { return 7; }

  constructor(options) {

    if ( !options || typeof options !== 'object' ) {
      throw new Error('options are required, and must be an object');
    }

    if (!options.url) {
      throw new Error('options must include a url property');  
    }

    this.url         =   options.url;
    this.headers     =   options.headers || [ { 'Content-Type' : 'application/json' } ];
    this.level       =   options.level || Logger.ERROR;
    this.batch_size =   options.batch_size || 10;
    this.messages   =   [];

  }

  send(messages) {    
    var xhr = new XMLHttpRequest();
    xhr.open('POST', this.url, true);

    this.headers.forEach(function(header){      
      xhr.setRequestHeader(
        Object.keys(header)[0],
        header[Object.keys(header)[0]]
      );
    });

    var data = JSON.stringify({
      context   :   navigator.userAgent,
      messages  :   messages
    });    
    xhr.send(data);
  }

  log(level, message) {
    if (level <= this.level) {
      this.messages.push({
        level : level,
        message : message
      });      
      if (this.messages.length >= this.batch_size) {
        this.send(this.messages.splice(0, this.batch_size));        
      }
    }
  }

  error(message) {
    this.log(Logger.ERROR, message);
  }

  warn(message) {
    this.log(Logger.WARN, message);
  }

  info(message) {
    this.log(Logger.INFO, message);
  }

  debug(message) {
    this.log(Logger.DEBUG, message);
  }

}

Usage is simple:

var logger = new Logger({
  url : 'http://example.com/api/batch-logger',
  batch_size : 5,
  level : Logger.INFO
});

logger.debug('This is a debug message'); // No effect
logger.info('This is an info message');
logger.warn('This is a warning');
logger.error('This is an error message');
logger.log(Logger.WARN, 'This is a warning');

Self-Hosted Server-Based Options

Errbit

Errbit is an open-source, self-hosted solution for capturing errors. It’s implemented in Ruby and uses MongoDB for storage.

If you want to give Errbit a quick spin, there’s a Chef cookbook or a Dockerfile you can use. There’s also an online demo you can try out.

To sign in to the online demo, use the e-mail demo@errbit-demo.herokuapp.com and the password password.

SaaS Server-Based Options

There are a number of SaaS solutions for logging. These include Loggly, track.js, ErrorCeption, Airbrake and New Relic.

Let’s take a brief look at a few such solutions.

Loggly

Loggly is one of a number of these SaaS solutions. I’m going to use it as an example because it’s easy and free to get started. With the free plan you can log up to 200MB per day, and the data is stored for 7 days.

To use Loggly from a client-side application, you’ll need to include the following snippet:

<script type="text/javascript" src="http://cloudfront.loggly.com/js/loggly.tracker.js" async></script>
<script>
  var _LTracker = _LTracker || [];
  _LTracker.push({'logglyKey': 'YOUR-LOGGING-KEY',
  'sendConsoleErrors' : true });
</script>

Note: You’ll need to replace YOUR-LOGGING-KEY with the value specific to your application, which you’ll get when you’ve signed up and logged in, by going to Source Setup.

If you examine this code, you’ll see that the _LTracker object is initially instantiated as an array. This is a “shim” technique used in many analytics libraries, which means that you can call push() on it before the library has loaded. Any errors or messages you push onto that array will be queued up for when the library becomes available.

Usage is simple:

_LTracker.push(data);

You can use it to send a snippet of text:

_LTracker.push( 'An error occured: ' + e.message );

Or, perhaps more usefully, you can use JSON — for example:

try {
  // some operation
} catch (e) {
  _LTracker.push({
    level   : 'error',
    message : e.message,
    trace   : e.trace,
    context : navigator.userAgent
  });
}

While a fairly basic solution, you could simply use the following code to capture errors:

window.onerror = function(message, file, line) {        
  _LTracker.push({
    context: navigator.userAgent,
    error: message,
    file: file,
    line: line
  });
};

There are some limitations to this approach. Line numbers are virtually useless if you have subtly different builds, or when you’re minifying your JS code.

You’ll also notice in the Loggly snippet above that sendConsoleErrors is set to TRUE, which will automatically log certain errors for you, without having to send them manually. For example, the following will get sent to Loggly if a RequireJS timeout occurs:

{
  "category": "BrowserJsException",
  "exception": {
    "url": "http://example.com/js/require.js",
    "message": "Uncaught Error: Load timeout for modules: main\nhttp://requirejs.org/docs/errors.html#timeout",
    "lineno": 141,
    "colno": 15
  },
  "sessionId": "xyz-123-xyz-123"
}

{track.js}

{track.js} is another SaaS solution for logging.

They offer a free plan; it’s limited to 10 errors per minute, 10,000 hits per month and your data is only stored for 24 hours. The most basic paid plan is $29.99 per month — you’ll find more details on their pricing page.

Note: a “hit” is recorded whenever the library is initialized.

Getting it set up is straightforward:

<!-- BEGIN TRACKJS -->
<script type="text/javascript">window._trackJs = { token: 'YOUR-TOKEN-HERE' };</script>
<script type="text/javascript" src="//d2zah9y47r7bi2.cloudfront.net/releases/current/tracker.js" crossorigin="anonymous"></script>
<!-- END TRACKJS -->

One you’ve pulled in the appropriate file and initialized the library, you can use methods such as track():

/**
  * Directly invokes an error to be sent to TrackJS.
  *
  * @method track
  * @param {Error|String} error The error to be tracked. If error does not have a stacktrace, will attempt to generate one.
  */
trackJs.track("Logical error: state should not be null");

try {
  // do something
} catch (e) {
  trackJs.track(e);
}

Or use the console, which will send the messages to the web service:

trackJs.console.debug("a message"); // debug severity
trackJs.console.log("another message"); // log severity

There’s a lot more you can do with {track.js} — check out the documentation for more information.

In Summary

Client-side logging is often overlooked, but it’s arguably just as important as logging server-side errors. However, there’s no doubt it’s more difficult to setup. There are plenty of options, however, a number of which we’ve looked at during the course of this article.

How do you handle logging in your client-side applications? Have you developed your own approach? Do you use something not covered here? Let me know in the comments.

Frequently Asked Questions (FAQs) on Client-Side Logging

What is the importance of client-side logging in web development?

Client-side logging plays a crucial role in web development. It helps developers track and understand the behavior of their applications in real-time. This is particularly useful in identifying and fixing bugs, improving application performance, and enhancing user experience. Client-side logging provides insights into how users interact with the application, what errors they encounter, and how the application performs in different environments. This information is invaluable in making data-driven decisions to improve the application.

How does client-side logging differ from server-side logging?

While both client-side and server-side logging are essential for monitoring application performance, they serve different purposes. Server-side logging focuses on tracking server operations, such as database queries and server responses. On the other hand, client-side logging is concerned with capturing user interactions, browser events, and performance metrics on the client’s device. This includes tracking errors that occur on the user’s browser, which may not be visible from the server-side.

What are the best practices for implementing client-side logging?

Implementing client-side logging effectively requires a strategic approach. First, determine what data you need to log. This could include user interactions, system events, or errors. Next, choose a suitable logging level. This determines the severity of events that will be logged. Also, consider using a logging library or service to simplify the process. Finally, ensure that your logging practices comply with privacy regulations to protect user data.

How can I handle sensitive data in client-side logging?

Handling sensitive data in client-side logging requires careful consideration. It’s crucial to avoid logging sensitive user information such as passwords, credit card numbers, or personal identification information. If it’s necessary to log such data, ensure it’s anonymized or encrypted. Also, consider implementing access controls to restrict who can view the logs.

What tools can I use for client-side logging?

There are several tools available for client-side logging. These include browser-based tools like console.log and more advanced libraries like LogRocket and Signoz. These tools provide features like real-time logging, error tracking, and performance monitoring. The choice of tool depends on your specific needs and the complexity of your application.

How can I use client-side logging to improve user experience?

Client-side logging can provide valuable insights into user behavior and application performance. By analyzing these logs, you can identify areas where users struggle, such as features that cause errors or slow down the application. You can then use this information to improve these areas and enhance the overall user experience.

Can client-side logging impact application performance?

While client-side logging provides valuable insights, it can impact application performance if not implemented correctly. Excessive logging can slow down the application and consume significant bandwidth. Therefore, it’s important to log only necessary data and consider using asynchronous logging to minimize impact on performance.

How can I manage and analyze logs from client-side logging?

Managing and analyzing logs from client-side logging can be challenging due to the volume of data. Using a log management tool can help. These tools aggregate logs from different sources, making it easier to search and analyze the data. They also provide features like real-time monitoring and alerts, which can help you quickly identify and resolve issues.

What are the challenges of client-side logging?

Client-side logging comes with several challenges. These include managing the volume of data, ensuring data privacy, and dealing with cross-browser compatibility issues. Additionally, client-side logs can be manipulated by users, which can lead to inaccurate data. Therefore, it’s important to validate and sanitize log data to ensure its reliability.

How can I secure my client-side logs?

Securing client-side logs is crucial to protect sensitive data. This can be achieved by encrypting log data, implementing access controls, and regularly auditing your logs for any suspicious activity. Also, consider using a secure log management service that complies with industry standards for data security.

Lukas WhiteLukas White
View Author

Lukas is a freelance web and mobile developer based in Manchester in the North of England. He's been developing in PHP since moving away from those early days in web development of using all manner of tools such as Java Server Pages, classic ASP and XML data islands, along with JavaScript - back when it really was JavaScript and Netscape ruled the roost. When he's not developing websites and mobile applications and complaining that this was all fields, Lukas likes to cook all manner of World foods.

error monitoringloggingnilsonj
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week