• 5 Ways of Error Handling in JavaScript

To write a program that works under the expected conditions is great, but making that program to behave when something unexpected happens - that is hard. So, what could it be? It could be programmers mistakes (like someone forgets to pass a required argument to a function) or genuine problems (if a program asks the user to enter a name and it gets back an empty string, for example). Well, one deals with genuine errors by having the code check for them and perform some suitable action.

This article will cover the exploration of error handling in JavaScript, beyond the bare necessities for handling exceptions. He you'll find the pitfalls, good practices, asynchronous code and Ajax. The focus will be on client-side JavaScript. And after reading this you might think twice the next time you see a nice try...catch block.

"Bomb"

This simulates an exception that gets thrown as a TypeError. Here is such module's definition:
// scripts/error.js

function error() {
  var foo = {};
  return foo.bar();
}

An empty object named foo is being declared by this function, and bar() doesn’t get a definition. By using a unit test let’s verify that this will detonate a bomb:

// tests/scripts/errorTest.js

it('throws a TypeError', function () {
  should.throws(error, TypeError);
});

With test assertions in Should.js (the assertion library) this unit test is in Mocha (a test runner). Beginning with it('description') test ends with a pass / fail in should. The unit tests don’t need a browser, they run on Node. You might want to pay attention to the tests as they prove out key concepts in plain JavaScript.

Using npm t you can run the tests after cloning the repo and installing the dependencies. Also, you can run the individual test like this: ./node_modules/mocha/bin/mocha tests/scripts/errorTest.js

As you can see, error() defines an empty object then it tries to access a method. It throws an exception because bar() doesn’t exist within the object. This often happens with a dynamic language like JavaScript.

Evil handler

Let's move to the bad error handling. The handler on the button has been abstracted from the implementation. Here is what it looks like:

// scripts/badHandler.js

function badHandler(fn) {
  try {
    return fn();
  } catch (e) { }
  return null;
}

A fn callback is being received as a parameter by the handler. Then this callback gets called inside the handler function. Its usefulness shows the unit tests:

// tests/scripts/badHandlerTest.js

it('returns a value without errors', function() {
  var fn = function() {
    return 1;
  };

  var result = badHandler(fn);

  result.should.equal(1);
});

it('returns a null with errors', function() {
  var fn = function() {
    throw new Error('random error');
  };

  var result = badHandler(fn);

  should(result).equal(null);
});

So, if something goes wrong this bad error handler returns null. The callback fn() can be pointing to a legit method or a bomb. You can see below the click event handler that tells the rest:

// scripts/badHandlerDom.js

(function (handler, bomb) {
  var badButton = document.getElementById('bad');

  if (badButton) {
    badButton.addEventListener('click', function () {
      handler(bomb);
      console.log('Imagine, getting promoted for hiding mistakes');
    });
  }
}(badHandler, error));

Trying to figure out what went wrong is that much harder after only getting a null. This fail-silent strategy can range from bad UX to data corruption. You can even spend hours debugging the symptom but miss the try-catch block. This evil handler pretends that everything is OK by swallowing the mistakes in code. This will result in debugging for hours in the future. And it's totally impossible to figure out where everything went wrong in a multi-layered solution with deep call stacks. As you can already tell, it's pretty bad. Fortunately, JavaScript offers a more elegant way of dealing with exceptions.

Ugly handler

// scripts/uglyHandler.js

function uglyHandler(fn) {
  try {
    return fn();
  } catch (e) {
    throw new Error('a new error');
  }
}

The unit test below shows how it handles exceptions:

// tests/scripts/uglyHandlerTest.js

it('returns a new error with errors', function () {
  var fn = function () {
    throw new TypeError('type error');
  };

  should.throws(function () {
    uglyHandler(fn);
  }, Error);
});

The exception here gets bubbled through the call stack, and now errors will unwind the stack. This is actually very helpful in debugging. Looking for another handler the interpreter travels up the stack, and this opens many opportunities to deal with errors at the top of the call stack. But, the thing is, the original error gets lost, and there's the need to traverse back down the stack to figure out the original exception.

Well, you can handle the ugly handler with a custom error. By adding more details to an error you make it helpful. You just have to append the specific information about the error. For example:

// scripts/specifiedError.js

// Create a custom error
var SpecifiedError = function SpecifiedError(message) {
  this.name = 'SpecifiedError';
  this.message = message || '';
  this.stack = (new Error()).stack;
};

SpecifiedError.prototype = new Error();
SpecifiedError.prototype.constructor = SpecifiedError;


// scripts/uglyHandlerImproved.js

function uglyHandlerImproved(fn) {
  try {
    return fn();
  } catch (e) {
    throw new SpecifiedError(e.message);
  }
}


// tests/scripts/uglyHandlerImprovedTest.js

it('returns a specified error with errors', function () {
  var fn = function () {
    throw new TypeError('type error');
  };

  should.throws(function () {
    uglyHandlerImproved(fn);
  }, SpecifiedError);
});

More details are being added by the specified error which keeps the original error message. This improvement give a makeover to an ugly handler, and now it's useful.

Stack unwinding

By placing a try...catch at the top of the call stack you can unwind exceptions. For example:

function main(bomb) {
  try {
    bomb();
  } catch (e) {
    // Handle all the error things
  }
}

The execution is being halted by the interpreter in the executing context and unwinds. We can use an onerror global event handler. It looks something like this:

// scripts/errorHandlerDom.js

window.addEventListener('error', function (e) {
  var error = e.error;
  console.log(error);
});

The errors are being catched by this event handler within any executing context. It centralizes error handling in the code. Handlers can be daisy chained to handle specific errors. And if you follow SOLID principles this allows error handlers to have a single purpose. They can get registered at any time. The interpreter will cycle through all of the handlers. The code base gets freed from try...catch blocks which makes it easy to debug. Error handling has to be treated like event handling in JavaScript.

Stack capturing

When troubleshooting issues the call stack is very helpful. And the browser provides this information out of the box. The stack property is consistently available on the latest browsers, because it's not part of the standard.

Now, for example, you can log errors on the server:

// scripts/errorAjaxHandlerDom.js

window.addEventListener('error', function (e) {
  var stack = e.error.stack;
  var message = e.error.toString();

  if (stack) {
    message += '\n' + stack;
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/log', true);
  // Fire an Ajax request with error details
  xhr.send(message);
});

The code is kept DRY due to the fact that every error handler have a single purpose.

The event handlers get appended to the DOM in the browser, which means that if you are building a third party library, your events will coexist with client code. This is taken care by the window.addEventListener(), and it does not blot out existing events.

It lives inside a command prompt, and run on Windows. This message comes from Firefox Developer Edition 54. It’s visible what threw the exception and where when you look at this, which is good for debugging front-end code. Giving insight on what conditions trigger which errors it's possible to analyze logs.

A tiny catch is that you won't see any of the error details if you have a script from a different domain and enable CORS. This occurs when scripts are put on a CDN to exploit the limitation of six requests per domain, for example. The e.message will only say "Script error" which is not good. Error information in JavaScript is only available for a single domain.

The only solution is while keeping the error message to re-throw errors:

try {
  return fn();
} catch (e) {
  throw new Error(e.message);
}

When you rethrow the error back up, your global error handlers will do the rest. You just have to make sure that your error handlers are on the same domain. It even can be wrapped around a custom error with specific error information, which will keep the original message, stack, and custom error object.

Async handling

JavaScript rips asynchronous code out of the executing context, which means that the exception handlers have a problem:

// scripts/asyncHandler.js

function asyncHandler(fn) {
  try {
    // This rips the potential bomb from the current context
    setTimeout(function () {
      fn();
    }, 1);
  } catch (e) { }
}

And the unit test tells the rest:

// tests/scripts/asyncHandlerTest.js

it('does not catch exceptions with errors', function () {
  // The bomb
  var fn = function () {
    throw new TypeError('type error');
  };

  // Check that the exception is not caught
  should.doesNotThrow(function () {
    asyncHandler(fn);
  });
});

This unit test verifies that the exception doesn't get caught. As you can see, an unhandled exception occurs even though I have code wrapped around a try...catch.This statements work only within a single executing context. The interpreter has already moved away from the try...catch when an exception is thrown. This occurs with Ajax calls too.

The thing you can do is to try to catch exceptions inside the asynchronous callback:

setTimeout(function () {
  try {
    fn();
  } catch (e) {
    // Handle this async error
  }
}, 1);

This will work, but the improvement wouldn't hurt, because try...catch blocks get tangled up all over the place and the V8 engine ( the JavaScript engine used in the Chrome browser and Node) discourages the use of try…catch blocks inside functions.

It was said before that the global error handlers operate within any executing context. You just need to add an error handler to the window object and it's done. It will keep your async code clean.

Conclusion

When it comes to error handling there might be two approaches. You can either ignore errors or fail-fast and rewind them. There's no need to hide problems which may occur in any program. Errors are inevitable, it's what you do about them that counts.

Become Awesome

  • No Ads
  • No Limits
  • More Content

Welcome to CheckiO - games for coders where you can improve your codings skills.

The main idea behind these games is to give you the opportunity to learn by exchanging experience with the rest of the community. Every day we are trying to find interesting solutions for you to help you become a better coder.

Join the Game