DEV Community

jser
jser

Posted on • Updated on

How to detect infinite loops in JavaScript?

Suppose we allow users to write some code and run the code in browser, how to avoid infinite loops? 🤔 Since it is a common mistake.
Alt Text

We need to use iframe for security concerns.

We need to sandbox the environment to avoid breaking the whole apps.

Blob url works but blob urls have the same origin as current web page, meaning the code copied from somewhere else actually have the access to the APIs, which is not good.

So data URIs is the best approach, we could use sandbox to disable features like popups .etc, which is great.

iframes and parent windows share the same thread.

The infinite loop in the iframe causes the whole window to freeze, because it is on the same thread with parent.

So there is no way to detect timeout on parent window, since it will never be called if infinite loop happens.

Web Worker doesn't have DOM API

For pure JavaScript functions without DOM API usage, worker might be a good choice to do timeout detecting.

But for our case, we need DOM API, so Web Worker is not an option.

Final approach, we need to modify users' code

Most common infinite loops are caused by while/for, if we inject some detecting code into the loop body, we actually could throw some errors when too many loop calls are detected.

Like code below

function abc() {
  while (true) {
    console.log(Date.now())
  }
}
Enter fullscreen mode Exit fullscreen mode

We could try to inject some code at the beginning and at the while loop body.

let __count = 0
const __detectInfiniteLoop = () => {
  if (__count > 10000) {
    throw new Error('Infinite Loop detected')
  }
  __count += 1
}

function abc() {
  while (true) {
    console.log(Date.now())
    __detectInfiniteLoop()
  }
}
Enter fullscreen mode Exit fullscreen mode

generate new code with AST traversing

We could use following 3 packages.

  1. @babel/parser - to parse user input code to AST
  2. @babel/traverse - to search for/while loop
  3. @babel/generator - to generate new code after the injection.

First, we parse the user's code

import { parse } from '@babel/parser'
const ast = parse(code)
Enter fullscreen mode Exit fullscreen mode

Then we prepare the code to be injected.

The code has 2 parts, 1st part could be inserted at the head, so string is good enough, the second parts needs to be inserted into the AST, so we parse it as well as the users' code.

const prefix = `
  let __count = 0
  const __detectInfiniteLoop = () => {
    if (__count > 10000) {
      throw new Error('Infinite Loop detected')
    }
    __count += 1
  }
`

const detector = parse(`__detectInfiniteLoop()`)
Enter fullscreen mode Exit fullscreen mode

Then we just look for for/while loops and insert the detector above.

import traverse from '@babel/traverse'
traverse(ast, {
  ForStatement: function (path) {
    path.node.body.body.push(...detector.program.body)
  },
  WhileStatement: function (path) {
    path.node.body.body.push(...detector.program.body)
  },
  DoWhileStatement: function (path) {
    path.node.body.body.push(...detector.program.body)
  }
})
Enter fullscreen mode Exit fullscreen mode

Now the code is modified, last step is transform it into string and run in iframe.

import generate from '@babel/generator'
const newCode = prefix + generate(ast).code
Enter fullscreen mode Exit fullscreen mode

This doesn't cover all the cases

Above approach just cover the common infinite loops from while/for, and checks the loop count, which is good enough for us.

But we know it is not perfect, please tell us if you have better ideas.

Hope it helps, see you next time.

Top comments (0)