Robust Network Code with window.setTimeout

by Michael CarterAugust 11th, 2008

In Orbited, we sometimes send multiple events in a single payload, particularly with the long-polling and polling transports. After we parse the data of each event, we call some event handler that a user has attached to the Orbited connection. Imagine code that looks something like this:

function rawOnReceive(rawData) {
    frames = parseFrames(rawData);
    for (var i = 0; i < frames.length; ++i) {
        self.onread(frames[i]);
    }
}

Unfortunately, sometimes user code can throw an exception. If this happens, we don’t want to stop sending the remainder of the frames, so we revised our original code to look more like this:

function rawOnReceive(rawData) {
    frames = parseFrames(rawData);
    for (var i = 0; i < frames.length; ++i) {
        try {
            self.onread(frames[i]);
        }
        catch(e) { }
    }
}

But now we have a problem — that is, if the user code fails, we hide the error, and then dispatch the next packet. This is a terrible solution because it will be next to impossible for users to track down bugs.

Consider window.setTimeout, a mechanism generally used to schedule calls after a set amount of time has expired. It has a side effect though; it moves code out of the callstack. So we use setTimeout with 0ms as the specified timeout in order to make the appropriate onread calls without worrying whether they throw exceptions or not:

function rawOnReceive(rawData) {
    frames = parseFrames(rawData);
    for (var i = 0; i < frames.length; ++i) {
        window.setTimeout(function() { self.onread(frames[i]) }, 0);
    }
}

This means that all of the onread callbacks will be scheduled to be executed, but none of them will actually execute until our code, the rawOnReceive function, finishes. Unfortunately, there is a second problem — the value of i used for each onread callback will be the final value of i. We need to hold on to each value separately for each callback. Consider this code:

function rawOnReceive(rawData) {
    frames = parseFrames(rawData);
    for (var i = 0; i < frames.length; ++i) {
        (function() {
            var j = i;
            window.setTimeout(function() { self.onread(frames[j]) }, 0);
        })();
    }
}

Note the extra function and the “var j = i” — Our solution is to create a closure that saves each value of i as locally scoped variable j. This code will allow us to make all the appropriate onread calls, whether or not one calls user code that causes an exception.

When I first tried this out, I thought it would fail due to the ordering of the messages — after all, we go through a lot of trouble in Orbited to guarantee message order. It would be truly tragic if we destroyed that ordering right at the last possible moment. But its actually not a problem whatsoever. Regardless of how the various browsers implement setTimeout, they have managed to do it in a way that preserves ordering if multiple consecutive calls have the same timeout value. Of course, I would feel more comfortable if this was documented somewhere!

12 Responses to “Robust Network Code with window.setTimeout”

  1. Jim Cook Says:

    In what browser does window.setTimeout() take a delay value _before_ the function parameter?

    http://developer.mozilla.org/en/docs/DOM:window.setTimeout

  2. henrah Says:

    Another approach, which minimizes reliance on execution queuing and undocumented functionality, is to execute the user code in the current callstack but to throw errors asynchronously so they do not halt the loop. This approach also reduces the need to create a new closure for every step of the loop, since setTimeout is only called when there is an error in user code.

    here is my implementation (hopefully not mangled by the blog engine):


    function throwLater(exception) {
    window.setTimeout(0, function () { throw exception });
    };

    function rawOnReceive(rawData) {
    frames = parseFrames(rawData);
    for (var i = 0; i < frames.length; ++i) try {
    self.onread(frames[i]);
    } catch (ex) {
    throwLater(ex);
    }
    }

  3. henrah Says:

    Edit to previous comment: the arguments to setTimeout are backwards in my code (it should be (fn, time) not (time, fn)). They are also backwards in the article though. =)

  4. Frank DENIS Says:

    Didn’t you reverse the arguments of window.setTimeout? :)

  5. Frank DENIS Says:

    Didn’t you reverse the arguments of window.setTimeout() ?

  6. Martin Says:

    Is it a good idea to use ‘j’ as a timeout parameter ?
    i.e. setTimeout(j, …);

  7. henrah Says:

    A third approach is to delegate your error handling, and error postponement, to a second function (in this case delayErrors):
    function delayErrors(block) {
    try { return block() }
    catch (ex) {
    window.setTimeout(function(){throw ex}, 0);
    }
    };
    function rawOnReceive(rawData) {
    var frames = parseFrames(rawData),
    frame, i = 0;
    while (frame = frames[i++]) delayErrors(function() {
    self.onread(frame);
    });
    };

  8. Michael Carter Says:

    henrah: thanks for the tips. Also, I corrected the fn/time argument order reversal in the article — thanks!

  9. Martin Tyler Says:

    Do some of these ideas mean you lose the full stack trace of the exception (ie by re-throwing) that is available to some debuggers? I know that we have an option to run in a mode that will just allow the exception to be thrown ‘naturally’ because there can be issues with tricks like the above - for production you wouldnt want that and the above tricks are better than just hiding the problem

  10. Jacob Rus Says:

    another (more common; maybe more readable?) way to do:

    function rawOnReceive(rawData) {
    frames = parseFrames(rawData);
    for (var i = 0; i < frames.length; ++i) {
    (function() {
    var j = i;
    window.setTimeout(function() {self.onread(frames[j])}, 0);
    })();
    }
    }

    is:

    function rawOnReceive(rawData) {
    frames = parseFrames(rawData);
    for (var i = 0; i < frames.length; ++i) {
    (function(i) {
    window.setTimeout(function() {self.onread(frames[i])}, 0);
    })(i);
    }
    }

  11. Jacob Rus Says:

    Hmm. That doesn’t like pre/code blocks. Maybe this will work?

    function rawOnReceive(rawData) {
      frames = parseFrames(rawData);
      for (var i = 0; i < frames.length; ++i) {
        (function(i) {
          window.setTimeout(function() {self.onread(frames[i])}, 0);
        })(i);
      }
    }

  12. bruno Says:

    Examples ????


Copyright 2015 Comet Daily, LLC. All Rights Reserved