Asynchronous Error Handling

Asynchronous callbacks happen asynchronously.
http://jsbin.com/laraqozi/1/edit

This is pretty beginner stuff, or so I thought, But after having fixed a bug in YUI that cost hundreds of man hours, it seems worth discussing.

function doSomethingBad() {
  throw Error("oh no!")
}

function deferCallback(f) {
  try {
    setTimeout(f, 100);
  } catch(ex) {
    // We never get here becauase setTimeout completes normally.
    alert("error!");
  }
}

deferCallback(doSomethingBad);

The call to setTimeout in the try block succeeds without error, passing it the function.

Later, when the browser calls the callback that was passed to setTimeout, an error is thrown.

Similarly with event handler callbacks that throw errors, either from IO or from something as simple as onclick:

  document.addEventListener("click", doSomethingBad, false);

No error is thrown by attaching doSomethingBad; the error happens when doSomethingBad is triggered later on.

Asynchronous errors are harder to track down because the stack trace stops at the browser internal code. The setTimeout call completes normally and is not part of the error stack trace.

Uncaught Error: oh no! file.js 2:3  
doSomethingBad file.js 2:3  
_safeWrap      file.js 10:4  

FunctionDeclaration Identifier

One of the benefits of FunctionDeclaration is that the function's Identifier, required for FunctionDeclaration, is present in the stack. FunctionExpression may have an optional identifier, however NFE's are buggy in IE8.

The contrived example is pretty obvious about what it does: throw Error("..."). In reality, code that is throwing an error will be more complicated and may depend the state of other objects. Conditional breakpoints can help.

Async errors can't be caught in a sync try/catch, but can be caught by either wrapping the call to the callback in a try/catch and handling the error there, or by using a global onerror handler. But global onerror won't tell you more than developer tools' stack trace does and is only useful for graceful error-handling and reporting in the UI.

Generally, code that calls async callbacks should provide an error handler callback. For example in async requests:

makeRequest(url, {success: callback, error: errback});

In runtime script errors:

//-----------------------------------
// Deferred Callback Error Handling
//-----------------------------------
function deferCallback(f, errback) {
    var wrapped = asyncCallbackWrapper(f, errback);
    setTimeout(wrapped, 100);
}

function asyncCallbackWrapper(f, errback) {
  return _safeWrap;
  function _safeWrap() {
    try {
      f();
    } catch(ex) {
      errback(ex);
    }
  }
}

deferCallback(doSomethingBad, function errback(ex) {
  alert("doSomethingBad threw: " + ex.name + ", " + ex.message);
});

Exotic Objects

EcmaScript has historically kept its hands off the dirty details of host objects.

What is a Host Object?

In order for JavaScript to be in any way useful, we must have some things; some components to script. This is where the host environment and host objects come in. In a web browser, that host environment includes the DOM.


Host environments must define other things, too, such as the event loop, and memory management, neither of which are defined by EcmaScript.


EcmaScript 5 doesn't have much to say about host object, other than the fact that they exist and their behavior is "implementation-dependent".

4.3.8 host object

object supplied by the host environment to complete the execution environment of ECMAScript.

NOTE Any object that is not native is a host object.


EcmaScript 5 does not preclude the possibility that a host object may be implemented with native semantics, which means that it allows for two types of host objects:

  • host objects as native objects
  • host objects as not-native objects (do not use native semantics)


Methoda alert and setTimeout, for example, are supplied by the host environment, which makes them both host objects. In some implementations, alert is a native function object. However in many versions of MSIE, it is not.

So what's different about alert in IE? In IE, it's not a function, so call and apply won't work. Nor will propery access and assignment work. Up until IE9, host methods still can't [[Put]]. alert.call = 1; // error

Exotic Errors

alert.call(window, "hello"); // IE Error
alert.a=12; // IE <= 9 Error.


When the EcmaScript specification describes internal methods such as [[Get]] and [[Put]], it says that the behavior is "implementation dependent". That is a barn-door wide allows the impelementation or "browser" in our case, to do anything. Here are some more examples of host objects:

if(xhr.open) // Error in IE7.
var div = document.createElement("div");
div.offsetParent; // Error in IE7.

Like EcmaScript 3, EcmaScript 5, host objects can be anything.

EcmaScript 6 makes respectable strides in trying to define exotics, as they're now called.

4.3.6 ordinary object

object that has the default behaviour for the essential internal methods that must be supported by all objects.

4.3.7 exotic object

object that has some alternative behaviour for one or more of the essential internal methods that must be supported by all objects.

NOTE Any object that is not an ordinary object is an exotic object.


We now have away to describe all those weird features in older versions of IE, and the few
oddballs that still show up in other browsers, such as HTML5's document.all.

Type of val Result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Symbol "symbol"
Object (ordinary and does not implement [[Call]]) "object"
Object (standard exotic and does not implement [[Call]]) "object"
Object (implements [[Call]]) "function"
Object (non-standard exotic and does not implement [[Call]]) Implementation-defined. May not be "undefined", "boolean", "number", "symbol", or "string".

Which still doesn't say much about this:

typeof document.all == "undefined"; // true
document.all.myElementId; // object

Strategy and Abstract Factory: StyleTransition

Factory

I've previously explained simple factory in my article Revenge of the Factory Factory.

In this article, I'll explain how I used an Abstract Factory to implement a Strategy for StyleTransition. This six-year-old code is an excellent study in Object-oriented design and patterns. As always, Encapsulate the parts that vary.

Strategy

The strategy design pattern is an abstraction for behavior among a set of components. An algorithm is selected based on the type of data, the source of the data, user choice, or other discriminating factors.

For Example:

In any transition between two sets of styles, each of the individual styles will need a different Strategy to be transitioned. The Style Value Type depends on the Style Property.

Pseudocode:

color:      rgb(12, 44, 89)  -->  rgb(233, 197, 17);
marginLeft: 0                -->  -9px;
padding:    0                -->  12px;
zIndex:     1                -->  100

Each StyleTransition has an array of TransitionAdapters. TransitionAdapter base "class" is abstract. Each concrete TransitionAdapter "subclass" has a blendTo that represents the --> above, and which blends its style's start value to its end value, along a timeline, whose duration is supplied by the caller.

Animation
start()
stop()
...
run()
 
 
StyleTransition
adapters TransitionAdapter[]
run()
«abstract»
TransitionAdapter
prop
fromValue
toValue
blendTo()
LengthTransitionAdapter
blendTo()
PositiveLengthTransitionAdapter
blendTo()
ColorTransitionAdapter
blendTo()
OpacityTransitionAdapter
blendTo()

Understanding the Diagram

Each TransitionAdapter has properties prop, fromValue, and toValue (string values). Each subclass must implement blendTo.

The alternative to using the TransitionAdapterFactory Strategy would have been one very long blendTo method with a series of if-else statements that runs each time blendTo is called. Not only would that be hard to read, but because blendTo method is called tens and potentially hundreds of times for one animation, putting that dispatch into one method would be inefficient and extremely hard to debug.

OCP

Open-Closed Principle: Modules and functions should be open for extension but closed for modifications.

There are three more implementations of TransitionFactory not shown in the diagram above. More can be created at any time and added to the TransitionAdapterFactory.

Function blendTo must be defined by each TransitionAdapter subclass. Obviously, color blending is quite different than marginLeft or fontWeight, padding must disallow negative values, and opacity must consider IE's alpha filter. Each style type needs a different blendTo Strategy.

The StyleTransition does not know any implementation details of any blendTo methods; all it knows that its run method calls blendTo and passes it a rationalValue (0-1).

// StyleTransition's run method, called by its 
// superclass, Animation, which passes in rationalValue.
run : function(rationalValue) {
    var i = 0, adapters = this.adapters, len = adapters.length,
    style = this.style, adapter;
    while (i < len) {
        adapter = adapters[i++];
        style[adapter.prop] = adapter.blendTo(rationalValue);
    }
}

Each StyleTransition has a set of TransitionAdapter instances and each TransitionAdapter has a property to represent the style name, the fromValue, and the toValue.

StyleTransition's run method is called by its superclass, Animation. This is a technique called Inversion of Control (IoC), or "The Hollywood Principle: (Don't call us, we'll call you)." I'll cover that in a later example.

function TransitionAdapter(prop, fromValue, toValue) {
    this.prop = prop;
    this.fromValue = fromValue;
    this.toValue = toValue;
}

Each subclass must implement a blendTo method. Method blendTo is called repeatedly during the animation, blending the fromValue and toValue along the timeline which is represented by a rational value of 0 to 1.

function ColorTransitionAdapter(prop, fromValue, toValue) {
    if (!ColorRGB) { // If script was included in wrong order.
        ColorRGB = APE.color.ColorRGB;
    }
    var f = ColorRGB.fromString(fromValue),
        t = ColorRGB.fromString(toValue);

    TransitionAdapter.call(this, prop, f, t);

    // This is where we mix fromValue and toValue,
    // to avoid the creation of new ColorRGB for each frame.
    this.currentValue = new ColorRGB();
}

APE.extend(ColorTransitionAdapter, TransitionAdapter);

Abstract Factory

The difference between a Factory and an Abstract Factory is that the latter is used to create a family of related objects, where as a Factory Method creates only one type of object. The reasons for wanting an Abstract Factory generally has to do with similar or shared behavior. Here, the shared behavior is serving a StyleTransition with objects that have a fromValue, toValue, and a blendTo method.

var TransitionAdapterFactory = {
  fromValues : function(prop, fromValue, toValue) {
    var adapter;
    if (positiveLengthExp.test(prop)) {
        adapter = PositiveLengthTransitionAdapter;
    } else if (colorExp.test(prop)) {
        adapter = ColorTransitionAdapter;
    } else if (lengthExp.test(fromValue)) {
        adapter = LengthTransitionAdapter;
    } else if (prop === OPACITY) {
        adapter = useFilter ? FilterTransitionAdapter : OpacityTransitionAdapter;
    } else if (prop == "fontWeight" && intExp.test(fromValue)
            && intExp.test(toValue)) {
        adapter = FontWeightTransitionAdapter;
    } else if (prop === "visibility" && noVisibilityExp.test(fromValue)
            || prop == "display" && fromValue == "none") {
        adapter = ImmediateThresholdTransitionAdapter;
    } else {
    // Return an object that sets toValue on completion.
    adapter = ThresholdTransitionAdapter;
    }
    return new adapter(prop, fromValue, toValue);
  }
};

Real World

This is not just some theory, this approach has been carefully and specifically designed to solve the problem of style transitions and has been used professionally on high-traffic sites. Source and Examples.

CSS Transitions should be favored, where applicable.

初心