Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Ruby cannot execute JavaScript with Kernel#eval: that method parses Ruby code. To load JavaScript held in a Ruby string, pass the string to a JavaScript runtime. The two documented Ruby approaches are ExecJS, which provides a common interface over installed runtimes, and MiniRacer, which embeds V8 in a Ruby process.

Use ExecJS.eval for a one-off expression, ExecJS.compile for reusable functions, or a persistent MiniRacer::Context when you specifically want embedded V8.

What “load JavaScript from a string” means in Ruby

A Ruby string is only text until a JavaScript engine parses it. Ruby itself has no JavaScript parser, so this does not work:

js = "1 + 2"
eval(js) # evaluates Ruby syntax, not JavaScript

Ruby’s Kernel#eval documentation describes evaluation of Ruby expressions, optionally in a Ruby Binding. It is not a JavaScript loader.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#1 Best Overall

Choose a runtime first, then give that runtime your source string. The runtime may be a separately installed executable (through ExecJS) or an engine embedded in the Ruby process (MiniRacer).

Option 1: evaluate a one-off string with ExecJS

Install and require the gem

Add ExecJS to your bundle:

gem "execjs"

Then run bundle install and require it:

require "execjs"

Evaluate an expression

ExecJS.eval is the shortest path for a string containing one expression:

require "execjs"

result = ExecJS.eval("'red yellow blue'.split(' ')")

p result
# => ["red", "yellow", "blue"]

The returned value is converted to a Ruby value when ExecJS can represent it. Keep the JavaScript source in a variable when it comes from a file, database, or another application component:

javascript = <<~JS
  ({ name: "Ada", roles: ["admin", "author"] })
JS

value = ExecJS.eval(javascript)
p value
# => {"name"=>"Ada", "roles"=>["admin", "author"]}

For an expression that is an object literal, wrapping it in parentheses avoids parsing it as a statement block.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reuse functions with an ExecJS context

When the string defines functions or contains a library that you will call repeatedly, compile it once:

require "execjs"

source = <<~JS
  function add(a, b) {
    return a + b;
  }
JS

context = ExecJS.compile(source)
result = context.call("add", 20, 22)

puts result
# => 42

compile creates a context containing the supplied source; call invokes a named JavaScript function with Ruby arguments. This avoids reparsing the same definitions for every call.

Load a JavaScript file into the string

require "execjs"

source = File.read("lib/calculator.js", encoding: "UTF-8")
context = ExecJS.compile(source)
puts context.call("add", 7, 5)

Your file must define the function in the context’s top-level scope, for example function add(a, b) { return a + b; }. Module systems such as Node’s require and browser globals are not supplied automatically by the common ExecJS interface.

Which JavaScript runtime does ExecJS use?

ExecJS presents a common API and automatically selects an available runtime. Its README lists Node.js, Bun, JavaScriptCore, Windows Script Host/JScript, Duktape, Rhino, V8/MiniRacer, and GraalVM JavaScript as supported possibilities; the actual choice depends on your environment. See the ExecJS README for the current list and setup details.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Inspect or select a runtime

To see the selected runtime:

require "execjs"
puts ExecJS.runtime.name

You can assign a runtime in Ruby:

ExecJS.runtime = ExecJS::ExternalRuntime.new(
  name: "Node.js",
  command: "node",
  runner_path: ExecJS.root.join("support", "node_runner.js")
)

In practice, prefer a runtime adapter already provided by your ExecJS version and deployment. You can also request a runtime with the EXECJS_RUNTIME environment variable. Verify the exact runtime name and executable on the target machine rather than assuming that a runtime available on a laptop exists in production.

ExecJS limits you must design around

Lowest-common-denominator JavaScript

ExecJS intentionally abstracts several engines. Runtime behavior and supported features therefore vary. Its documentation recommends relying on ES3-level features unless you have checked the selected runtime and its APIs. Modern syntax or host APIs may work with one runtime and fail with another.

No general event loop

ExecJS does not guarantee a full JavaScript event loop. Timers such as setTimeout and setInterval are not defined through the interface. Code that expects browser APIs, DOM objects, timers, network access, or Node-specific modules needs a different integration approach.

Not a security sandbox

Do not treat automatic runtime selection as a safe sandbox for arbitrary input. ExecJS warns that runtimes have different sandbox properties and advises against evaluating input that you would not be comfortable passing to Ruby’s eval. Only execute trusted, application-controlled JavaScript, or isolate untrusted code with a separately designed security boundary.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Option 2: load the string in MiniRacer’s embedded V8

Create a context and evaluate source

MiniRacer embeds V8 and exposes a persistent context:

require "mini_racer"

context = MiniRacer::Context.new
context.eval("var adder = (a, b) => a + b;")
result = context.eval("adder(20, 22)")

puts result
# => 42

The first evaluation defines JavaScript state; later evaluations reuse that same context. This is useful when you need V8-specific behavior or several calls against initialized code.

Evaluate a Ruby string and call a function

require "mini_racer"

source = <<~JS
  function slugify(value) {
    return value.toLowerCase().replace(/[^a-z0-9]+/g, "-");
  }
JS

context = MiniRacer::Context.new
a = context.eval(source)
slug = context.call("slugify", "Ruby & JavaScript")
puts slug
# => ruby-javascript

MiniRacer’s documentation also describes context timeouts, a memory soft limit, filename information for stack traces, and attaching Ruby functions to JavaScript. Consult its current documentation for the supported Ruby and platform matrix before selecting a version.

ExecJS or MiniRacer?

Requirement ExecJS MiniRacer
Runtime choice Common interface; selects or can be configured to use an available runtime Embedded V8 context
One expression ExecJS.eval context.eval
Reusable code ExecJS.compile and context.call Keep one MiniRacer::Context and evaluate or call repeatedly
Portability Depends on the runtime installed and its feature set Depends on MiniRacer’s supported Ruby/platform and V8 packaging
Resource controls No universal event-loop or sandbox guarantee Documented timeout and memory soft-limit controls

Choose ExecJS when portability across engines and a uniform API matter. Choose MiniRacer when your application specifically wants V8 embedded in the Ruby process and its documented controls. In either case, test the exact Ruby version, operating system, JavaScript syntax, and deployment image you will ship.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common errors and fixes

ExecJS::RuntimeUnavailable

Cause: ExecJS cannot find a usable JavaScript runtime. Fix: install a supported runtime such as Node.js in the deployment image, confirm its executable is on PATH, then print ExecJS.runtime.name. If several runtimes are present, configure the intended one explicitly.

Syntax errors after switching environments

Cause: the selected engine does not support syntax or APIs used by your source. Fix: check the selected runtime, reduce the code to the supported feature set, or use MiniRacer when embedded V8 is a better fit.

“setTimeout is not defined”

Cause: ExecJS does not promise a browser-style event loop. Fix: rewrite the code as synchronous functions, or run it in an environment that actually provides the required event loop.

Function is not found

Cause: the function was defined inside a local wrapper, module export, or different context. Fix: define it at the context’s accessible top level and call the exact name; for MiniRacer, keep and reuse the same context.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Unexpected Ruby evaluation

Cause: calling Ruby eval instead of a JavaScript runtime. Fix: replace it with ExecJS.eval, ExecJS.compile, or MiniRacer::Context#eval.

Untrusted code execution

Cause: JavaScript engines and adapters do not automatically create a security boundary. Fix: do not pass user-supplied source to these APIs. If untrusted execution is unavoidable, design a separately isolated service with a security model appropriate to your threat.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Operational guidance

  • Compile or initialize reusable source once per worker or request scope that safely owns the context; avoid reparsing large libraries unnecessarily.
  • Pin gem and runtime versions in deployment, and run tests against the same image used in production.
  • Return plain data (strings, numbers, arrays, and objects) across the Ruby/JavaScript boundary to minimize conversion surprises.
  • Log the selected runtime and JavaScript exceptions, but avoid logging secrets embedded in source or arguments.
  • Set MiniRacer’s documented timeout and memory controls where long-running or resource-heavy scripts could affect a worker.

Or skip the browser setup

If your goal is to obtain a rendered website image rather than execute arbitrary JavaScript inside Ruby, ScreenshotNeo makes one HTTP request and returns a PNG, JPEG, WebP, or PDF. It accepts cookie/consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status.

For a direct call, see the ScreenshotNeo API documentation:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same endpoint can be called from Ruby:

require "net/http"
require "uri"

uri = URI("https://api.screenshotneo.com/v1/shot")
uri.query = URI.encode_www_form(access_key: "YOUR_API_KEY", url: "https://stripe.com")
File.binwrite("shot.webp", Net::HTTP.get(uri))

It also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I execute JavaScript without installing Node.js?

Yes, if you use MiniRacer’s embedded V8 and your platform is supported. ExecJS, however, needs an available runtime adapter, which may be Node.js, Bun, JavaScriptCore, or another documented engine.

How do I pass Ruby values to JavaScript?

Pass values as arguments to ExecJS context.call or MiniRacer context.call; use JSON-compatible data types to keep conversion predictable.

Does this load browser DOM code?

No. These APIs evaluate JavaScript engines, not a browser page. DOM, browser events, timers, and network APIs are unavailable unless the chosen host explicitly provides them.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.