Error Logging

Mistakes happen. It’s a fact of life. But, mistakes are often a precursor to growth…as long as you learn from them!

Early on after launching our service, the SparkPost support team occasionally received feedback from customers who tried to sign up for one of our paid tiers, explaining that the transaction failed. Not only did that stink for our customers, but it was a real pain point for us as well. We knew that sort of error couldn’t continue unchecked, so we decided to get to the bottom of this frustrating problem.

We integrate with a third party for payment processing, and the third-party system sometimes responds to a request with what’s essentially an unknown error. Super helpful! In most cases, we were able to surface a more helpful error to the user based on logic that parses the response. But up until a few months ago, we had zero visibility into the unknown errors. We had no way to help our customers, and no way to learn from the problem! After looking at a few more third-party solutions, we decided to leverage functionality our current stack already provided.

Some updates to our Angular-based UI and our Nginx web server was all it took. And while we were at it, we went ahead and used the approach to log uncaught exceptions, too. In the front-end portion of our application, we built a simple service for posting errors to Nginx. We just have to get the username from the session, along with the error response and data posted, and post it back to our logs. (Of course, for compliance reasons, we never deal with actual credit card information; our third-party service takes care of that.) No need to catch a rejected promise in the service either, because for our case, we don’t have a need for handling failed log attempts.

(function() {
  let dependencies = ['ApplicationConfig', 'Session', '$http'];
  class ErrorLogger {
    constructor() {
      dependencies.forEach((dep, index) => this[dep] = arguments[index]);
      this.uiErrorApiBase = this.ApplicationConfig.errorLogs.apiBase;
     * Gets username from the current session
     * @returns {String|undefined} session username when exists; undefined otherwise
     getUsername() {
      return (this.Session.getData('user') || {}).username;
     * Posts to Nginx ui error logs
     * @param {Object} error response
     * @param {Object} request data
    post(err, data) {
      let log = {
        Error: err
      if (this.getUsername()) {
        log.Username = this.getUsername();
      if (data) {
        log.RequestBody = data;
      this.$ + '/errorlog', JSON.stringify(log));
  ErrorLogger.$inject = dependencies;
  angular.module('', []).service('ErrorLogger', ErrorLogger);

From the Nginx side, we’re able to customize our log format by leveraging the built in directives and variables available in the core module as well as the log and proxy modules. In our case, we’re using the access_log and log_format directives, and the $msec variable from the log module. Then $proxy_add_x_forwarded_for from the proxy module. We get $http_user_agent and $request_body from the core module. With that in place, we can easily see information about the request, in addition to the data being sent from the UI.

log_format uierr '$msec "$proxy_add_x_forwarded_for" $http_user_agent "$request_body"';

Finally, back to our front-end code and the case of uncaught exceptions. Angular comes with a global application exception handler $exceptionHandler that does exactly that. In our case, we use the $provide service’s $decorator method to add additional behavior. It’s called in the config phase callback, so that the decorator can intercept the service creation, and we use the $delegate method to retain the exception handlers base functionality.

$provide.decorator('$exceptionHandler', function($delegate, $injector) {
  var postException = _.throttle(function(err, data) {
    var ErrorLogger = $injector.get('ErrorLogger');, data);
  }, 300, {leading: true});
  return function(exception, cause) {
    var stack = (exception.stack || {}).toString();
    postException(exception.toString(), {stack: stack});
    $delegate(exception, cause);

As any JavaScript developer will know, we have a lot of new tools at our disposal today. But, by knowing your existing tools well, you can arrive at simple, clean solution that addresses your problem without having to pull in extra dependencies, and without the need to learn additional APIs.

Now, whenever our support team needs to assist customers with their subscriptions, we’re able to quickly access additional details, and as an added bonus, we have an easy way to gain insight into exceptions within our application to prevent future occurrences! Learning from this previously frustrating error turned out to be an opportunity for improving the customer experience—and for making the internals of our application better, as well.