Better Errors Through JavaScript Inheritance
ne of the more frustrating things in JavaScript has always been how Errors are handled. They are difficult to check for, harder to extend and often times have cryptic messages providing little insight into what has gone wrong.
Uncaught TypeError: undefined is not a function
Everyone has seen this at one time or another. It isn't very helpful, and these types of errors are actually difficult to account for in JavaScript applications. The only error handling in JavaScript that the language allows is generic try/catch
, where you can catch an error, but trying to recover from specific errors is difficult to do. Large in part the the fact that you can throw anything, not just errors. You can throw any Object
, and it is legal syntax. While JavaScript does have the instanceof operator that would allow you the check error types, it checks the prototype chain, so it can have some odd behaviors.
Inheriting from the Error Object has some unusual behavior
Using the instance operator isn't quite what we are looking for. If the object you are comparing against is anywhere in the prototype chain, it will return true.
MyError instanceof MyError // true
MyError instanceof Error // true
This is more of what you would like to do in JavaScript. Knowing that certain operations can produce specific error types that can be accounted for. Unfortunately, trying to subclass Error also has some unusual behavior.
function Exception(name, message){
Error.call(this, name, message);
};
Exception.prototype = Object.create(Error.prototype);
var e = new Exception("BrokeException","This is broke");
throw e;
// Error
Neither the name or message were captured here. More Over, it doesn't have a stacktrace. This is worse that just throwing an instance of Error
or a String
for that matter. We can do better than this, but it will take a little work.
Exception Class
'use strict'
var util = require('util')
function quickmerge(...args) {
var i = 1, target, obj
target = arguments[0]
for (const arg of args) {
target = {
...target
, ...arg
}
}
return target
};
function Exception(opts = {}) {
var tmp // temporary error object
this.options = quickmerge({
name: 'Exception'
, message: ''
, code: 1000
, type: 'base_exception'
}, opts)
// force this thing to really be an error
tmp = Error.call(this, this.options.message)
tmp.name = this.name
this.stack = tmp.stack
// cleanup
tmp = undefined
};
Exception.prototype = Object.create(Error.prototype)
Object.defineProperties(Exception.prototype, {
name: {
get: function() {
return this.options.name
}
}
, code: {
get: function() {
return this.options.code
}
}
, type: {
get: function() {
return this.options.type
}
}
, message: {
get: function() {
return this.options.message
}
}
})
Exception.prototype.toString = function() {
return util.format('[%s] %s', this.name, this.message)
}
// output
BetterException: This is better
at new Error (<anonymous>)
at Error.Exception (repl:10:20)
at repl:1:9
at REPLServer.self.eval (repl.js:110:21)
at repl.js:249:20
at REPLServer.self.eval (repl.js:122:7)
at Interface.<anonymous> (repl.js:239:12)
at Interface.EventEmitter.emit (events.js:95:17)
at Interface._onLine (readline.js:202:10)
at Interface._line (readline.js:531:8)
This is quite a bit better. Not only is there a stack trace, but it has the right name, and message. Additionally, we have given our error a bit more meta data so developers can figure out what they are dealing with - a code
and an internal type
. Not perfect, but certainly a step in the right direction in terms of being able to deal with errors in a more logical and structured way.