divillysausages.com

Making sense of AS3 Runtime errors at runtime

Today, on Stack Overflow, I answered this question. The problem resolved around release builds of Flash not providing enough information on runtime errors. If you've had any experience with runtime errors, then you probably know the http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/runtimeErrors.html page quite well. If not, you're welcome :)

Now obviously you can just go to the page everytime you have an error and look up the message and description, but perhaps sometimes you can't - no internet, it's a non-technical user that triggered it, you're just not arsed - so I figured that this could be automated. Hence, the RuntimeErrorUtil class.

How it works

Basically nothing happens until you request a translation using either the toString( error:Error, callback:Function = null ):String function or the toStringFromID( errorID:int, callback:Function = null ):String one. If no data has been loaded yet, it makes a call to the Adobe reference site, and scrapes the HTML. Then, using some amazing parsing skills, it extracts the relevant data for all the errors listed.

Because it involves a web call, the results aren't instant if the data hasn't been loaded yet, hence the callback parameter in the method signatures. It takes a Function with one parameter of type String. After the data has been loaded, all subsequent calls to toString() and toStringFromID() are instant.

To get around the initial delay, you can call loadFromWeb( callback:Function = null ):void to preload the results. The callback parameter is called when everything has loaded.

Offline mode

But what about when you don't have internet access? In that case, you can use the loadFromXML( x:XML ):void function to load a pre-filled XML object with all your data. How do you get this data? Using the toXML():XML function (boom) or just download the XML attached (up-to-date as of the date this post was published).

Bonus code

Most of the time, you should be catching your Errors, but every once in a while you either miss or forget one. To stop unwanted behaviour, you can make use of the uncaughtErrorEvents property of the Loader/LoaderInfo classes. Use the Loader one when you're loading in other SWFs, and the LoaderInfo one for your main SWF (or within the SWF you're loading if you want it to deal with its own errors).

main.loaderInfo.uncaughtErrorEvents.addEventListener( UncaughtErrorEvent.UNCAUGHT_ERROR, this._onUncaughtError );
...
private function _onUncaughtError( e:UncaughtErrorEvent ):void
{
	// do something here with e.error (can be an Error or ErrorEvent), like log 
	// the error, perhaps even going crazy and using the RuntimeErrorUtil class
	e.preventDefault(); // stops the error dialog box showing up
}

Note that above, main is your Document class, not the Stage. You can access the uncaughtErrorEvents property as soon as the SWF is fully loaded. Also note that using a catch-all error handler isn't perhaps the greatest thing to do, and can be considered a bad practice. Still, it exists for a reason :)

Another handy bit of code is getting the current stack trace - if you print out a runtime error, it's useful to know where it happened. Obviously, you can just call getStackTrace() on the Error object if you still have it, but a more general form is available:

public static function getStackTrace():String
{
	var stack:String = null;
	try
	{
		throw new Error;
	}
	catch ( e:Error )
	{
		stack = e.getStackTrace();
	}
	return stack;
}

For this example, you just need to ignore the first line of the stack trace, which will always be the call to this function. NOTE that in release builds, you won't actually have the file path or line number, but at least it will give you the call order.

The code

Update 30/11/2013: I updated the code to fix a bug when calling toString() or toStringFromID when we've made the web call but haven't received the results yet, and when we call it after having received the results, but we pass a callback

Copypasta, or download the class below:

package
{
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.utils.Dictionary;
	import mx.utils.StringUtil;
	
	/**
	 * A util class to help dealing with runtime errors
	 * @author Damian Connolly
	 */
	public class RuntimeErrorUtil 
	{
		
		/********************************************************************************/
		
		private static var m_errors:Dictionary 								= new Dictionary; 	// the dictionary where we keep our errors, once they're loaded
		private static var m_callbacks:Vector.<RuntimeErrorUtilCallback>	= null; 			// any callbacks to call when we've loaded our data
		private static var m_loadFromWebCallback:Function					= null;				// the callback to call when we've finished loading our data from the web
		private static var m_triedLoading:Boolean							= false;			// have we made the call to load the errors yet?
		private static var m_receivedResponse:Boolean						= false;			// have we received our response from the server yet?
		
		/********************************************************************************/
		
		/**
		 * Loads the errors from the Adobe site, from the url 
		 * http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/runtimeErrors.html.
		 * You can call this to preload error data, so when you look for it, you don't need to wait
		 * @param callback The callback to call when we've finished loading our data. It should take no parameters
		 */
		public static function loadFromWeb( callback:Function = null ):void
		{
			// if we've already loaded, do nothing
			if ( RuntimeErrorUtil.m_triedLoading )
				return;
				
			// check our callback
			if ( callback != null && callback.length != 0 )
			{
				trace( "[RuntimeErrorUtil: Can't store the loadFromWeb callback, as it should take no parameters" );
				callback = null;
			}
			RuntimeErrorUtil.m_loadFromWebCallback = callback;
				
			// create our url loader and get our data
			var urlLoader:URLLoader = new URLLoader;
			urlLoader.addEventListener( IOErrorEvent.IO_ERROR, RuntimeErrorUtil._onIOError );
			urlLoader.addEventListener( SecurityErrorEvent.SECURITY_ERROR, RuntimeErrorUtil._onSecurityError );
			urlLoader.addEventListener( Event.COMPLETE, RuntimeErrorUtil._onLoadComplete );
			try
			{
				urlLoader.load( new URLRequest( "http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/runtimeErrors.html" ) );
			}
			catch ( e:Error )
			{
				trace( "[RuntimeErrorUtil: An error occurred when trying to load our data from the web: " + e.errorID + ": " + e.name );
			}
		}
		
		/**
		 * Loads the error data from an XML object, in the format provided by the
		 * toXML function. Using this means that you don't need an internet connection to
		 * recover the data
		 * @param x The XML containing all our error data
		 */
		public static function loadFromXML( x:XML ):void
		{
			RuntimeErrorUtil.m_triedLoading = true; // so we don't load from web
			
			// our data should be in the format
			// <runtimeErrors>
			//		<error id="1000"><![CDATA[...]]></error>
			//		...
			// </runtimeErrors>
			for each( var ex:XML in x.error )
			{
				var id:int 						= int( ex.@id );
				RuntimeErrorUtil.m_errors[id]	= unescape( ex.toString() );
			}
		}
		
		/**
		 * Parses a runtime error using the data that we've loaded. If we haven't loaded it in yet,
		 * then a call to loadFromWeb is made, and you should use the callback parameter
		 * @param error The error that we're trying to translate
		 * @param callback The callback to call when our data has been loaded. It should take one
		 * parameter of type String. If null, and our data has been loaded, then the Error string
		 * is just returned directly
		 * @return The Error string, or an empty String if we haven't loaded our data yet
		 */
		public static function toString( error:Error, callback:Function = null ):String
		{
			return RuntimeErrorUtil.toStringFromID( error.errorID );
		}
		
		/**
		 * Parses a runtime error using the data that we've loaded. If we haven't loaded it in yet,
		 * then a call to loadFromWeb is made, and you should use the callback parameter
		 * @param errorID The ID for the error that we're trying to translate
		 * @param callback The callback to call when our data has been loaded. It should take one
		 * parameter of type String. If null, and our data has been loaded, then the Error string
		 * is just returned directly
		 * @return The Error string, or an empty String if we haven't loaded our data yet
		 */
		public static function toStringFromID( errorID:int, callback:Function = null ):String
		{
			// check our callback
			if ( callback != null && callback.length != 1 )
			{
				trace( "[RuntimeErrorUtil: Can't store a callback for errorID " + errorID + " as the callback should take one parameter of type String" );
				callback = null;
			}
					
			// if we've received our response, then just return the results
			if ( RuntimeErrorUtil.m_receivedResponse )
			{
				var error:String 	= RuntimeErrorUtil.m_errors[errorID]; // can be null if our errorID isn't in our table
				error				= ( error != null ) ? error : "";
				
				// if we have a callback, call it
				if ( callback != null )
					callback( error );
				return error;
			}
				
			// we haven't received our results yet, so if we have a callback, store it
			if ( callback != null )
			{
				if ( RuntimeErrorUtil.m_callbacks == null )
					RuntimeErrorUtil.m_callbacks = new Vector.<RuntimeErrorUtilCallback>;
				RuntimeErrorUtil.m_callbacks.push( new RuntimeErrorUtilCallback( errorID, callback ) );
			}
			
			// if we haven't tried loading yet, do so
			if ( !RuntimeErrorUtil.m_triedLoading )
			{
				RuntimeErrorUtil.loadFromWeb();
				RuntimeErrorUtil.m_triedLoading = true;
			}
			return "";
		}
		
		/**
		 * Saves our loaded data to XML, so that you can provide it as an offline data source. This
		 * avoids internet calls, and is the quickest way to rebuild the data. Use loadFromXML() to
		 * load the data back in
		 * @return An XML object describing our error data
		 */
		public static function toXML():XML
		{
			// go through our dictionary and save our errors
			var x:XML = new XML( <runtimeErrors /> );
			for ( var key:* in RuntimeErrorUtil.m_errors )
			{
				var ex:XML = new XML( <error id={key} /> );
				ex.appendChild( new XML( "<![CDATA[" + RuntimeErrorUtil.m_errors[key] + "]]>" ) );
				x.appendChild( ex );
			}
			return x;
		}
		
		/********************************************************************************/
		
		// reads our error table
		private static function _readErrorTable( rawHTML:String ):String
		{
			var index:int 				= 0;
			var tables:Vector.<String> 	= new Vector.<String>;
			while ( index != -1 )
			{
				// find the start tag
				index = rawHTML.indexOf( "<table", index );
				if ( index == -1 )
					break;
					
				// find the end tag
				var start:int = index;
				index = rawHTML.indexOf( "</table>", index );
				if ( index == -1 )
					break;
				index += 8; // to add the </table> to the end
					
				// add the table to our vector
				tables.push( rawHTML.substring( start, index ) );
			}
			
			// now go through and remove the biggest table, which is probably the one
			// containing our datas
			var biggest:String = "";
			for each( var s:String in tables )
			{
				if ( s.length > biggest.length )
					biggest = s; // the biggest will have our info
			}
			return biggest;
		}
		
		// called when there's been an IO error when we try to get our data
		private static function _onIOError( e:IOErrorEvent ):void
		{
			trace( "[RuntimeErrorUtil: An IOError occurred when trying to get our data: " + e.errorID + ": " + e.text );
			RuntimeErrorUtil.m_receivedResponse = true;
			RuntimeErrorUtil._cleanLoader( e.target as URLLoader );
		}
		
		// called when there's been a security error when we try to get our data
		private static function _onSecurityError( e:SecurityErrorEvent ):void
		{
			trace( "[RuntimeErrorUtil: A SecurityError occurred when trying to get our data: " + e.errorID + ": " + e.text );
			RuntimeErrorUtil.m_receivedResponse = true;
			RuntimeErrorUtil._cleanLoader( e.target as URLLoader );
		}
		
		// called when our data is loaded in
		private static function _onLoadComplete( e:Event ):void
		{
			RuntimeErrorUtil.m_receivedResponse = true;
			
			// get our loader and clean it up
			var urlLoader:URLLoader = ( e.target as URLLoader );
			RuntimeErrorUtil._cleanLoader( urlLoader );
			
			// first thing we need to do is find all the <tables> and strip our the biggest
			var table:String = RuntimeErrorUtil._readErrorTable( urlLoader.data );
			
			// now go through our table, and parse out the details.
			// The errors are in the form of:
			// <td class="summaryTableSecondCol">
			//		<a name="1000"></a><b>1000</b>
			// </td>
			// <td class="summaryTableCol" valign="top">
			//		The system is out of memory.
			// </td>
			// <td class="summaryTableLastCol">
			//		Flash needs more memory to compile your code than your system has available. 
			//		Close some of the applications or processes running on your system.
			// </td>
			var index:int 	= 0;
			var start:int	= 0; // used for substringing
			while ( index != -1 )
			{
				// the error id
				// find the start tag
				index = table.indexOf( "<td class=\"summaryTableSecondCol\">", index );
				if ( index == -1 )
					break;
				index += 34; // to add the <td class="summaryTableSecondCol">
				
				// find the end tag
				start = index;
				index = table.indexOf( "</td>", index );
				if ( index == -1 )
					break;
				index += 5; // to add the </td>
				
				// parse our error ID
				var errorID:int = int( table.substring( start, index ).match( /[0-9]+/ )[0] );
				
				// the message
				// find the start tag
				index = table.indexOf( "<td class=\"summaryTableCol\" valign=\"top\">", index );
				if ( index == -1 )
					break;
				index += 41; // to add the <td class="summaryTableCol" valign="top">
				
				// find the end tag
				start = index;
				index = table.indexOf( "</td>", index );
				if ( index == -1 )
					break;
				
				// parse our message
				var msg:String = StringUtil.trim( table.substring( start, index ) );
				index += 5; // to add the </td>
				
				// the description
				// find our start tag
				index = table.indexOf( "<td class=\"summaryTableLastCol\">", index );
				if ( index == -1 )
					break;
				index += 32; // to add the <td class="summaryTableLastCol">
				
				// find the end tag
				start = index;
				index = table.indexOf( "</td>", index );
				if ( index == -1 )
					break;
					
				// parse our description
				var desc:String = table.substring( start, index );
				index += 5; // to add the </td>
				
				// to clean up the description a bit, we need to replace bad markup or unhelpful descriptions
				// with something more useful
				desc = desc.replace( "&nbsp;", "" ); 	// remove all the &nbsp;
				desc = desc.replace( "See the <a href=\"#note\" >note</a> at the bottom of this table. &#42;", "This error indicates that the ActionScript in the SWF is invalid. If you believe that the file has not been corrupted, please report the problem to Adobe." );
				desc = StringUtil.trim( desc );			// remove the whitespace at the start and end
				
				// remove some of the other whitespace formatting that adobe have added, as instead of just
				// letting the text break around the <td> cell, they've added a combination of tabs, newlines
				// and tons of spaces, so you end up with a line like "this is     		a description". This 
				// breaks the code formatting a bit, but it's not such a big deal
				desc = desc.replace( /[\t\r\n]/g, "" ); // tabs, newlines etc
				desc = desc.replace( /  +/g, " " );		// multiple spaces
				
				// add our error into our dictionary
				var errorMsg:String = "RuntimeError #" + errorID + ": " + msg;
				if ( desc.length > 0 )
				{
					// add a full stop if one is necessary
					if ( errorMsg.charAt( errorMsg.length - 1 ) != "." )
						errorMsg += ". " + desc;
					else
						errorMsg += " " + desc;
				}
				RuntimeErrorUtil.m_errors[errorID] = errorMsg;
			}
			
			// dispatch our callback if we have one
			if ( RuntimeErrorUtil.m_loadFromWebCallback != null )
			{
				RuntimeErrorUtil.m_loadFromWebCallback();
				RuntimeErrorUtil.m_loadFromWebCallback = null;
			}
			
			// finally, fire off any waiting callbacks
			RuntimeErrorUtil._clearCallbackQueue();
		}
		
		// fires off any waiting callbacks
		private static function _clearCallbackQueue():void
		{
			if ( RuntimeErrorUtil.m_callbacks == null )
				return;
				
			// go through and call all our callbacks, destroying as we go along
			for each( var cb:RuntimeErrorUtilCallback in RuntimeErrorUtil.m_callbacks )
			{
				cb.callback( RuntimeErrorUtil.toStringFromID( cb.errorID ) );
				cb.destroy();
			}
			RuntimeErrorUtil.m_callbacks.length = 0;
		}
		
		// cleans all the event listeners from our loader so it can be gc'd
		private static function _cleanLoader( urlLoader:URLLoader ):void
		{			
			urlLoader.removeEventListener( IOErrorEvent.IO_ERROR, RuntimeErrorUtil._onIOError );
			urlLoader.removeEventListener( SecurityErrorEvent.SECURITY_ERROR, RuntimeErrorUtil._onSecurityError );
			urlLoader.removeEventListener( Event.COMPLETE, RuntimeErrorUtil._onLoadComplete );
		}
		
	}

}

// helper class for dealing with callbacks
internal class RuntimeErrorUtilCallback
{
		
	/********************************************************************************/
	
	public var errorID:int 			= 0;	// the error ID that we're trying to translate
	public var callback:Function	= null;	// the callback to call when we're ready
		
	/********************************************************************************/
	
	/**
	 * Creates a new RuntimeErrorUtilCallback
	 * @param errorID The error ID for the error that we're trying to translate
	 * @param callback The callback to call when we've loaded
	 */
	public function RuntimeErrorUtilCallback( errorID:int, callback:Function ):void
	{
		this.errorID 	= errorID;
		this.callback	= callback;
	}
	
	/**
	 * Destroys the callback and clears it for garbage collection
	 */
	public function destroy():void
	{
		this.callback = null;
	}
}

Files

Comments

mrLove

great article ! :)

Submit a comment

* indicates required