Budibase/budibase · error · JsTimeoutError

JS_TIMEOUT_ERROR

JS_TIMEOUT_ERROR

Error message

Timed out while executing JS

What it means

The JS runner executes user JavaScript inside a VM with an execution timeout. When the underlying VM aborts with 'Script execution timed out.', init converts it into a typed JsTimeoutError (code JS_TIMEOUT_ERROR) so callers can recognize the script ran too long and was terminated instead of crashing with a generic error.

Source

Thrown at packages/server/src/jsRunner/index.ts:48

            .withHelpers()
            .withBuffer()
            .withSnippets(bbCtx?.snippets)

        // Persist isolate in context so we can reuse it
        if (bbCtx && !bbCtx.vm) {
          bbCtx.vm = vm
          bbCtx.cleanup = bbCtx.cleanup || []
          bbCtx.cleanup.push(() => vm.close())
        }

        // Because we can't pass functions into an Isolate, we remove them from
        // the passed context and rely on the withHelpers() method to add them
        // back in.
        const { helpers, snippets, ...rest } = ctx
        return vm.withContext(rest, () => vm.execute(js))
      } catch (error: any) {
        if (error.message === "Script execution timed out.") {
          throw new JsTimeoutError()
        }
        throw error
      }
    })
  })

  if (env.LOG_JS_ERRORS) {
    setOnErrorLog((error: Error) => {
      logging.logWarn(
        `Error while executing js: ${JSON.stringify(serializeError(error))}`
      )
    })
  }
}

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Review the executed JS for non-terminating loops or unbounded recursion and fix the termination condition.
  2. Move heavy data processing out of JS bindings (e.g. into automations or pre-computed data).
  3. Raise the JS runner timeout configuration if the script legitimately needs more time.
  4. Break large synchronous work into smaller batches processed across multiple executions.

Example fix

// before
let done = false
while (!done) { /* never sets done */ }
// after
for (let i = 0; i < rows.length; i++) {
  if (matches(rows[i])) { result = rows[i]; break }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Static sanity check before running user JS
function hasBoundedLoops(code: string): boolean {
  return !/while\s*\(\s*(true|1)\s*\)/.test(code)
}

Type guard

function isJsTimeout(err: unknown): err is Error & { code: string } {
  return err instanceof Error && (err as { code?: string }).code === "JS_TIMEOUT_ERROR"
}

Try / catch

try {
  const result = await jsRunner.init(context).execute(js)
} catch (err) {
  if (isJsTimeout(err)) {
    // mark the binding as timed out, skip or surface a user-facing message
  } else { throw err }
}

Prevention

When it happens

Trigger: Executing a user-defined JS snippet (via the runner's init/execute path, e.g. triggered through publishAsyncEvent) whose script exceeds the configured VM execution timeout — typically infinite loops, unbounded recursion, or very heavy synchronous computation.

Common situations: Automation/JS bindings with a `while` loop that never terminates; accidentally calling the script recursively; transforming a huge dataset synchronously; regression after adding expensive per-row JS logic; timeout configured too low for legitimately heavy scripts.

Understand the failure class

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/109ed06125ff5781. Report an issue: GitHub.