Last Updated: February 25, 2016
·
4.483K
· viatropos

How to flatten out deeply nested recursion to prevent stack overflow on IE8

IE8 only allows your execution stack to be like 30-100 function calls, which basically prevents taking advantage of recursion. Some examples where you use a lot of recursion include building recursive descent parsers, or even more simple cases such as executing tests recursively:

https://github.com/visionmedia/mocha/issues/502

Below I compare two ways of accomplishing the same thing, one using the much simpler version that uses recursion (but breaks on <=IE8 when recursion is too deep), and the more obtuse but <=IE8 compatible version.

Recursive structure

function a(n) {
  return b(n + 1) + b(n + 2) + b(n + 3);
}

function b(n) {
  return n + c(n);
}

function c(n) {
  return n + n;
}

function parse(n) {
  return a(n);
}

Executing the function gives 18:

console.log(parse(0)); // 18

If you throw an error in c and inspect the stack, you'll see that it includes a and b since it's recursive:


/**
 * Error
    at c (./recursion.js:11:15)
    at b (./recursion.js:7:14)
    at a (./recursion.js:3:32)
    at parse (./recursion.js:16:10)
 */

function c(n) {
  try { throw new Error } catch (e) { console.log(e.stack) }
  return n + n;
}

This is a problem for <= IE8, where it only allows a very shallow execution stack (just a few hundred function calls, whereas modern JS engines seem to allow thousands). Here is an example:

https://github.com/visionmedia/mocha/issues/502

In the case you end up using recursion, such as for the tower expression parser (a recursive descent parser basically), this can become a problem in IE8 and below. You can fix this by flattening out the recursion:

function a(n, stack) {
  stack.push([b, n + 1]);
  stack.push([b, n + 2]);
  stack.push([b, n + 3]);
}

function b(n, stack) {
  stack.push([identity, n]);
  stack.push([c, n]);
}

function c(n, stack) {
  stack.push([identity, n + n]);
}

function identity(val) {
  return val;
}

function parse(n) {
  var stack = [];
  // push initial functions into stack
  a(n, stack);

  var result = 0;

  while (stack.length) {
    var fn = stack.shift();
    var val = fn[0].apply(null, [fn[1], stack]);
    if (val) result += val;
  }

  return result;
}

console.log(parse(0));

Now if you inspect the stack at c, it is shallow:

/**
 * Error
    at C (./recursion.js:47:15)
    at Parse (./recursion.js:64:21)
 */

function c(n, stack) {
  try { throw new Error } catch (e) { console.log(e.stack) }
  stack.push([identity, n + n]);
}

This approach is definitely not as pretty, nor as easy to understand, as the recursive version, but it does solve the problem of having too deep a stack trace for incompetent browsers. Hopefully, though, IE8 will be phased out and we can move on :)

Another solution would be to use setImmediate and create an async API. But for our particular use case, making a client-side template renderer async would just slow down rendering, and that's something I want to avoid.

Do you know of a better way to accomplish this?

1 Response
Add your response

Don't use IE8. Fixed.

over 1 year ago ·

Have a fresh tip? Share with Coderwall community!

Post
Post a tip
Did you know?

JavaScript on a page runs with the privileges of that origin. One altered library, tag manager, or snippet can read inputs, change what people see, and call your APIs as the user.

Security insights from Cside