Skip to main content
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Gareth Sykes
Ben Nadel at Scotch On The Rocks (SOTR) 2011 (Edinburgh) with: Gareth Sykes

Experimenting With Error Sub-Classing Using ES5 And TypeScript 2.1.5

By
Published in Comments (4)

I really enjoy the idea of having custom Error types in JavaScript. And, I really like the idea of wrapping one error inside of another error, creating nested Russian-doll-style objects as exceptions bubble up to the surface of the application. But, as much as JavaScript is super dynamic, the constraints of TypeScript make this a bit harder. As such, I wanted to experiment with sub-classing the native Error object in TypeScript, specifically with an ES5 transpilation target.

Run this demo in my JavaScript Demos project on GitHub.

To be clear, if your browser / JavaScript runtime supports ES6, extending the native Error object becomes a trivial matter because ES6 now supports the extension of native Constructors. But, ES6 support hasn't landed in all of the browsers, especially some of the slightly older ones; so, I still use ES5 as my transpilation target in System.js. And, since ES5 doesn't support the simplified Class syntax, extending the Error object in ES5 means manually adjusting the Prototype chain.

Of course, in TypeScript, we can use the simplified Class syntax instead of mucking with the Prototype because TypeScript takes care of those details behind the scenes during the transpilation process. Unfortunately, this still doesn't work with native constructors like Error and Array. As such, we have to apply a little more elbow grease to get custom errors working in TypeScript and ES5.

After a few mornings of trial-and-error, the solution that I came up with is to use a "hacky intermediary Class" that blurs the lines between TypeScript's syntactic sugar and the actual JavaScript implementation of prototypal inheritance. With this approach, the custom Error Sub-Class doesn't extend "Error" directly, but rather, extends "ErrorSubclass". This ErrorSubclass then takes care of writing up the prototype chain, capturing the stacktrace, and ensuring proper TypeScript type validation.

Let's take a look at this intermediary ErrorSubclass:

interface ErrorSubclass extends Error {
	// Here, we are defining an interface - ErrorSubclass - that shares the same name as
	// our class. In TypeScript, this allows us to define aspects of the Class that we
	// don't actually have to implement in the class definition. In this case, we're
	// using the interface to tell TypeScript that ErrorSubclass extends Error even
	// though our class definition doesn't use "extends". We're doing this here so that
	// TypeScript doesn't try to implement the extends on the class itself - we're going
	// to do it explicitly with the .prototype.
}

// I am a "hacky" class that helps extend the core Error object in TypeScript. This
// class uses a combination of TypeScript and old-school JavaScript configurations.
class ErrorSubclass {

	public name: string;
	public message: string;
	public stack: string;

	// I initialize the Error subclass hack / intermediary class.
	constructor( message: string ) {

		this.name = "ErrorSubclass";
		this.message = message;

		// CAUTION: This doesn't appear to work in IE, but does work in Edge. In
		// IE, it shows up as undefined.
		this.stack = ( new Error( message ) ).stack;

	}

}

// CAUTION: Instead of using the "extends" on the Class, we're going to explicitly
// define the prototype as extending the Error object.
ErrorSubclass.prototype = <any>Object.create( Error.prototype );

The first thing you might notice here is that we have an Interface and a Class that both use the same identifier, ErrorSubclass. This is a feature of TypeScript that allows us to declare an interface for a class that doesn't necessarily implement said interface "cleanly." In this case, we're telling TypeScript that our ErrorSubclass extends the Error interface; but, as you can see, our actual ErrorSubclass definition doesn't extend Error. Instead, we're explicitly setting the prototype of our ErrorSubclass to extend the prototype of the Error constructor.

NOTE: This is the basic approach to Custom Errors outlined in the Mozilla Developer Network (MDN).

The interface, in this case, allows for better Type validation in TypeScript itself, such as allowing a sub-class to be auto-cast to a super-class. And, the explicit prototype chain then allows for things like "instanceof" to work with both the sub-class and the super-class (Error).

CAUTION: We can only monkey with the prototype chain in this case because our ErrorSublcass defines no public methods. If it defined a public method, our attempt to explicitly set the prototype chain would break the class definition.

Once we have this hybrid TypeScript / JavaScript class definition in place, we can then subclass the Error object using the "extends" concept in our custom error TypeScript classes. To see this in action, let's create a custom AppError class that builds on the Error object - by extending ErrorSubclass - adding additional reporting properties.

In the following demo, we're going to throw an Error object. Then, we're going to catch it and wrap it in a custom Error sub-class - AppError - which we'll then rethrow:

// NOTE: If you browser supports ES6, you can simply "extends Error". But, not all
// browsers support ES6 class definitions yet.

interface ErrorSubclass extends Error {
	// Here, we are defining an interface - ErrorSubclass - that shares the same name as
	// our class. In TypeScript, this allows us to define aspects of the Class that we
	// don't actually have to implement in the class definition. In this case, we're
	// using the interface to tell TypeScript that ErrorSubclass extends Error even
	// though our class definition doesn't use "extends". We're doing this here so that
	// TypeScript doesn't try to implement the extends on the class itself - we're going
	// to do it explicitly with the .prototype.
}

// I am a "hacky" class that helps extend the core Error object in TypeScript. This
// class uses a combination of TypeScript and old-school JavaScript configurations.
class ErrorSubclass {

	public name: string;
	public message: string;
	public stack: string;

	// I initialize the Error subclass hack / intermediary class.
	constructor( message: string ) {

		this.name = "ErrorSubclass";
		this.message = message;

		// CAUTION: This doesn't appear to work in IE, but does work in Edge. In
		// IE, it shows up as undefined.
		this.stack = ( new Error( message ) ).stack;

	}

}

// CAUTION: Instead of using the "extends" on the Class, we're going to explicitly
// define the prototype as extending the Error object.
ErrorSubclass.prototype = <any>Object.create( Error.prototype );


// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //


interface AppErrorOptions {
	message: string;
	detail?: string;
	extendedInfo?: string;
	code?: string;
	rootError?: any;
}

class AppError extends ErrorSubclass {

	public name: string;
	public detail: string;
	public extendedInfo: string;
	public code: string;
	public rootError: any;


	// I initialize the AppError with the given options.
	constructor( options: AppErrorOptions ) {

		super( options.message );

		this.name = "AppError";
		this.detail = ( options.detail || "" );
		this.extendedInfo = ( options.extendedInfo || "" );
		this.code = ( options.code || "" );
		this.rootError = ( options.rootError || null );

	}


	// ---
	// PUBLIC METHODS.
	// ---


	// I am here to ensure that public methods can work with a Class that extends an
	// object that extends Error.
	public testPublicMethod() : string {

		return( "Public method exists on the Error sub-class." );

	}

}


// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //


try {

	throw( new Error( "Boom!" ) );

// NOTE: The value in a catch() block has an implicit ANY type and cannot be given a more
// specific type annotation. This makes sense because there's no way to consistently know
// the root cause of an error at compile time.
} catch ( error ) {

	logError( <Error>error );

	try {

		// Wrap the caught error in a custom AppError.
		throw(
			new AppError({
				message: "Something went horribly wrong!",
				detail: "You crossed the streams!",
				code: "gb",
				rootError: error
			})
		);

	} catch ( nestedError ) {

		// NOTE: Using TypeScript to cast the "any" type in the catch-block to an
		// explicit AppError. This way, we can see if the AppError correctly fits into
		// the "Error" type expected by the logError() function.
		logError( <AppError>nestedError );

		// Test to make sure the public method works (ie, that inheritance worked
		// without screwing up the concrete class).
		console.info( nestedError.testPublicMethod() );

	}

}


// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //


// I log the given error object to the console.
function logError( error: Error ) : void {

	group( "Log Error" );
	console.log( "Error instance: ", ( error instanceof Error ) );
	console.log( "AppError instance: ", ( error instanceof AppError ) );
	console.log( "Message: " + error.message );

	// If we're dealing with an AppError, we can output additional properties.
	// --
	// NOTE: In TypeScript, this IF-expression is known as a "Type Guard", and will
	// tell TypeScript to treat the "error" value as an instance of "AppError" for the
	// following block, which is great because we will get the extra type-protection
	// for the values on the AppError class.
	if ( error instanceof AppError ) {

		console.log( "Detail: " + error.detail );
		console.log( "Extended Info: " + error.extendedInfo );
		console.log( "Code: " + error.code );
		console.log( "Root Error: " + error.rootError.message );

	}

	// NOTE: The .stack property is only populated in IE 10+ AND, even then, only when
	// an error instance is thrown. Also, it looks like this might not get populated on
	// sub-classes in
	console.log( error.stack );
	groupEnd();

}


// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //


// I safely define a "console.group()" method, which is not available in some IE.
function group( name: string ) : void {

	console.group
		? console.group( name )
		: console.log( "- - - [ " + name + " ] - - - - - - - - - - - - - -" )
	;

}


// I safely define a "console.groupEnd()" method, which is not available in some IE.
function groupEnd() : void {

	console.groupEnd
		? console.groupEnd()
		: console.log( "- - - [ END ] - - - - - - - - - - - - - -" )
	;

}

As you can see, our AppError class uses pure TypeScript, extending the ErrorSubclass "intermediary class" that we discussed above. But, there a number of more subtle details that are worth pointing out.

First, we pass (and down-cast) both the core <Error> object and our custom <AppError> object to a method that expects an argument of type Error. This demonstrates that TypeScript sees AppError as a true subclass of the Error object, thanks to the "interface hack" that we used when defining the intermediary ErrorSubclass.

Second, the instanceof operator works as expected with both the Error and the AppError instances. This works because we explicitly set the prototype chain in our intermediary ErrorSubclass definition.

Third, we can use instanceof within the log-method to create what's known as a Type Guard in TypeScript. If we branch our logic using instanceof, TypeScript is smart enough to know that the scope of said IF-block should treat the value as the tested class Type. This way, we actually get the full power of type-safety even though our log-method parameter was defined according to the super-class.

Anyway, when we run this code, we get the following output:

Custom error class / sub-class in TypeScript using ES5 compilation target.

As you can see, we were able to subclass the core Error object in TypeScript with an ES5 transpilation target. This was only possible because of the intermediary ErrorSubclass class that blurred the line between TypeScript and ES5 / JavaScript. This might seem like a lot of work; but, think about it - now that we have the ErrorSubclass defined, any other custom Error object should be trivial to create.

The one thing that seems to break with custom Error objects - regardless of TypeScript - is the .stack property in IE. It looks like IE won't populate this property (even in IE10+) until Microsoft switches over to the Edge browser.

To be clear, this is just the solution that I came up with for extending the core Error object in TypeScript - it's not necessary the best solution. It was just a bunch of trial-and-error. The nice thing about it, though, is that when you want to upgrade to an ES6-compliant runtime, you just need to replace "extends ErrorSubclass" with "extends Error" and everything else should just work.

Want to use code from this post? Check out the license.

Reader Comments

17 Comments

This is pretty cool. I was actually just thinking about this the other day, because I was looking at the idea of some app level error handler, where I could have it pop up messages when needed, hide those that dont, log to console when in a debug mode, and send data back to server where needed. Mostly just thinking about it from a high level "could I do it and how" perspective, but was actually thinking "this would be so much easier with having error types" to handle branching logic. Would definitely be easy for ES6.

This definitely is fun result of trial and error. Interesting to see how you can do it with the "intermediary class".

15,902 Comments

@John,

It's funny, I was really excited about this, and then I completely hit a mental wall that I haven't been able to get passed in the last few weeks. I feel completely blocked about how to handle errors in an application. I have all the ingredients, I feel like: Custom error objects and error wrapping -- but, when it comes to actually wiring it all together in my little app, I just can't see the clear picture.

Programming is hard! And feeling especially hard these last few weeks.

17 Comments

Yeah. It's one of those things when you want to have an "overall" solution that it gets tricky because you want to make sure you do it right, so you only have to do the bulk once (and then iterate).

I'm having similar issues, on a smaller scale. I'm trying to set up a messaging service in my app for doing toast messages to a user, your standard success / error / info sort of stuff. So my initial thought was to make a service that everyone can use and push messages to and then just have a component in the main area of the app that will display those, have them auto close, manage multiple, and everything. My only problem is around error handling, and essentially your problem. I want a good solution to manage that, so if I have a good solution there, it can handle the logic to push messages up to the user when it should vs any that should just log (such as a service level error I'll log, but when it gets back to a component, it will worry about the user). Just trying to think through how that 'might' interact, to try and save myself work down the road when I do have a full error handling solution.

15,902 Comments

@John,

Just wanted to check back in with you, see if you made any progress / came up with any path forward. I've continued to think a lot about error handling and tried to come up with holistic solution:

www.bennadel.com/blog/3273-considering-when-to-throw-errors-why-to-chain-them-and-how-to-report-them-to-users.htm

... but, it primarily applies to back-end workflows. I haven't really thought about this from a front-end viewpoint as much (ie, handling errors in the SPA).

I think your Service idea for a Toaster makes sense. It just becomes a question of how do you pipe errors into that toaster service - do you have to do this in every Controller / Component that makes calls to an API? I need to do more thinking on error handling on the client. This is not clear to me.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel