ExecJS and the Ruby side of running a JavaScript runtime
Run JavaScript code from Ruby
At a glance
- What is it?
- ExecJS evaluates JavaScript from Ruby by picking whichever runtime is available, ranging from Node and Bun to JavaScriptCore, Rhino and the Windows Script Host, and it deliberately exposes only the lowest common denominator of the language. The interesting part is what that constraint costs you.
- Who is it for?
- ExecJS is a good fit for a Ruby application that needs to run a small piece of JavaScript, or that already compiles assets through a JavaScript toolchain, because it removes the question of which JavaScript engine to depend on and works with whatever the host provides.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 73 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A runtime picker with a Ruby-shaped interface
ExecJS lets you run JavaScript code from Ruby. It automatically picks the best runtime available to evaluate your JavaScript program, then returns the result to you as a Ruby object.
That last clause is the point of the library. JavaScript values have to come back as Ruby values, and the conversion is where a naive bridge usually leaks type confusion. The short example in the README makes it concrete:
require "execjs"
ExecJS.eval "'red yellow blue'.split(' ')"
# => ["red", "yellow", "blue"]A JavaScript array of strings arrives in Ruby as an array of strings, not as a stringified blob. You are calling into a real JavaScript engine, so the semantics of the code you send are the semantics of that engine, and the boundary only appears at the marshalling step.
The runtime list is longer than most people expect. It includes therubyrhino, which is Mozilla Rhino embedded within JRuby, Duktape.rb as a Duktape interpreter, Node.js, Bun.sh described as a JavaScript runtime and toolkit designed for speed, Apple JavaScriptCore included with Mac OS X, the Microsoft Windows Script Host running JScript, Google V8, mini_racer as Google V8 embedded within Ruby, and GraalVM JavaScript used on TruffleRuby.
That spread is the actual feature. A gem that depends on Node forces Node onto every machine that installs it, including a production container built without it. ExecJS instead degrades to whatever is present, which is why it has survived as a dependency of asset pipelines for over a decade.
The audience is a Ruby developer whose application needs a JavaScript engine for one narrow job, most often compiling assets or evaluating a snippet from a template.
Compiled contexts and how arguments cross the boundary
There are two ways to use the library, and the second is the one that matters for anything nontrivial.
ExecJS.eval evaluates a string and hands back the result immediately. ExecJS.compile takes a source program and returns a context object that you can call repeatedly. The longer example in the README demonstrates the second form by fetching the CoffeeScript browser compiler and driving it:
require "execjs"
require "net/http"
source = Net::HTTP.get(URI("https://coffeescript.org/browser-compiler-legacy/coffeescript.js"))
context = ExecJS.compile(source)
context.call("CoffeeScript.compile", "square = (x) -> x * x", bare: true)The result is the compiled JavaScript source. What the example shows about the bridge is the argument passing: a Ruby string goes in as a JavaScript string, and a Ruby keyword argument, bare: true, arrives as a named argument on the JavaScript side. The README notes that the compiled output in this case carries no trailing semicolon and no source map comment when the bare option is set.
Compiling once and calling many times matters for performance and for state. A compiled context holds the program in memory, so a loop that calls into it a thousand times does not reparse or re-evaluate the source each time. It also means state can persist between calls, which is useful when you are driving a compiler or an interpreter rather than evaluating a pure expression.
For a Ruby code base that wants to reuse an existing JavaScript library rather than reimplement it, this is the mechanism. The context object is the handle, and call is the invocation, with Ruby arguments and Ruby keyword arguments mapped onto the JavaScript function signature.
One caveat is easy to miss. The example fetches its compiler over the network at runtime, which is a demonstration choice rather than a recommendation, but it does highlight that a compiled context is a live program you are driving, not a static function you are calling.
Forcing a runtime when autodetection picks the wrong one
Autodetection is convenient and it is also nondeterministic across environments. The README documents how to take control.
You can assign the runtime class directly:
ExecJS.runtime = ExecJS::Runtimes::NodeOr set the EXECJS_RUNTIME environment variable, which is the option that belongs in a deployment configuration rather than in code:
EXECJS_RUNTIME=Node ruby ...The full list of available runtime identifiers lives in lib/execjs/runtimes.rb, which is worth opening once if you are pinning, since the names are the class names under that namespace and the environment variable takes one of those names as a string.
Why would you pin? The straightforward answer is that the available runtimes differ in language support, and the README's own FAQ makes that point. Some runtimes such as Node implement many of the latest ES5 features, while older stock runtimes like JavaScriptCore on OSX and JScript on Windows may not. The advice given is to only count on ES3 features being available, and to prefer feature checking these APIs rather than hard coding support for specific runtimes.
That advice is correct but only half a solution, because feature checks fail differently across engines, and a code path that only ever runs under JScript will be the one nobody tests. Pinning the runtime converts an invisible difference between environments into an explicit, reviewable configuration line.
There is a second reason to pin: startup cost and behaviour. A runtime that ships with the operating system costs nothing to install but may be old, and a runtime you install yourself may not exist on every machine that runs the application.
The repository structure reflects the shape of the gem. There is lib/ for the code, test/ for the suite, a Rakefile for the tasks, the execjs.gemspec for packaging, a Gemfile for development, and CONTRIBUTING.md and MAINTAINING.md for contributors and for maintainers, which is a sensible split for a gem with a long life and many contributors.
What the lowest common denominator actually removes
The FAQ in the README is the most useful part of the documentation, because it is where the library states what it will not do.
Modules are the first exclusion. ExecJS provides the lowest common denominator interface to any JavaScript runtime, and the guidance is to use ExecJS when it does not matter which JavaScript interpreter your code runs in. If you want to access the Node API, the README points to commonjs.rb, which is designed to provide a consistent interface for that.
Timers are the second. Not all runtimes guarantee a full JavaScript event loop, so setTimeout, setInterval and other timers are not defined.
The third exclusion is broader and easier to miss. Some runtimes such as Node implement many of the latest ES5 features, but older stock runtimes like JavaScriptCore on OSX and JScript on Windows may not. The stated floor is that you should only count on ES3 features being available.
That is a real constraint in practice. ES3 predates JSON in the standard, lacks Array.prototype.forEach and map, has no strict mode, and its regular expression handling differs from what most JavaScript written today assumes. Code that runs fine under Node can throw a ReferenceError under a stock JScript runtime the moment you move it to a Windows host.
So the effective contract is: small, self-contained JavaScript that you control, evaluated to produce a value. A pure computation, a template helper, a small parser. The moment your JavaScript needs to import a module, schedule a callback, or use a language feature newer than ES3, ExecJS stops being the right abstraction and a runtime-specific integration becomes the honest choice.
Why ExecJS is not a sandbox
The README answers the sandboxing question with a flat no, and the reasoning behind it is short enough to repeat.
ExecJS should not be used for any security related sandboxing. The reason given is that runtimes are automatically detected, so each runtime has different sandboxing properties. And the practical advice is that you should not use ExecJS.eval on any inputs you would not feel comfortable Ruby eval-ing.
That framing is deliberate. Ruby eval is the mental model: if you would not eval the string in Ruby, do not eval it here. Whatever isolation the underlying engine provides is a side effect of which engine happened to be detected, not a guarantee the library makes, and a guarantee that varies by environment is not a guarantee at all.
For the same reason, the runtime-pinning advice above has a security dimension. A library that picks a different engine on a different host is a library whose behaviour changes with the host, and behaviour that changes with the host is hard to reason about when the input is attacker-controlled.
There is also a performance dimension to the autodetection that the README does not discuss. Some of the supported runtimes are embedded, some are separate processes, and the cost of starting one differs from another. A gem that silently launches a subprocess per evaluation on some machines and does an in-process call on others will behave very differently in production than on a workstation.
None of this makes ExecJS a bad tool for its stated job. It makes it a tool for a specific job, and the README is unusually clear about where that job ends.
Versioning, packaging and where to look next
ExecJS is MIT licensed, released under the MIT License, with the licence text at the repository root as MIT-LICENSE.
The release history shows a mature project with a recent burst of activity. The three most recent versions are v2.10.0 and v2.10.1, both on 2026-04-03, and v2.10.2 on 2026-08-12, which is also the date of the last push to the repository. The project is not archived. The version numbering is at 2.10 rather than 1.x, which under semantic versioning means the public interface is considered stable, so upgrading should not break your call sites.
The gem is installed the ordinary way:
$ gem install execjsand it is published on RubyGems as execjs, which is the package name you would add to a Gemfile.
The repository is deliberately small. Alongside lib/ and test/ there is the Rakefile for running the tasks, the execjs.gemspec, a Gemfile, and the two process documents, CONTRIBUTING.md for contributors and MAINTAINING.md for maintainers. The README notes that ExecJS is the work of dozens of contributors and invites pull requests, feature proposals and discussion, with CONTRIBUTING.md as the place to start.
That shape tells you something about the project. There is no native extension to compile, no runtime to vendor and no configuration file format to learn. The whole library is the runtime picker, the marshalling and the context object, and the only thing you need to understand beyond the README is the list of runtime names in lib/execjs/runtimes.rb if you decide to pin one.
Editorial conclusion
ExecJS is a good fit for a Ruby application that needs to run a small piece of JavaScript, or that already compiles assets through a JavaScript toolchain, because it removes the question of which JavaScript engine to depend on and works with whatever the host provides. It is a poor fit if you need Node's module system, timers, or any ES5 feature beyond the basics, and a poor fit if you want to sandbox untrusted code, since the README says explicitly that ExecJS should not be used for security related sandboxing. Before adopting, check what runtime your production hosts actually have, because the autodetection will pick something different on a developer laptop than on a minimal server image, and pin it with EXECJS_RUNTIME if the difference matters.
Frequently asked questions
How do I install ExecJS?
Install the gem with `gem install execjs`, or add execjs to your Gemfile. The gem is MIT licensed and its current release is v2.10.2, published on 2026-08-12.
Which JavaScript runtimes does ExecJS support?
The README lists therubyrhino, Duktape.rb, Node.js, Bun.sh, Apple JavaScriptCore, the Microsoft Windows Script Host running JScript, Google V8, mini_racer and GraalVM JavaScript used on TruffleRuby. ExecJS picks whichever one is available on the host.
How do I force ExecJS to use a specific runtime?
Assign ExecJS.runtime, for example `ExecJS.runtime = ExecJS::Runtimes::Node`, or set the EXECJS_RUNTIME environment variable, for example `EXECJS_RUNTIME=Node ruby ...`. The list of available runtime names is in lib/execjs/runtimes.rb.
Can I use require() or setTimeout inside ExecJS?
No. ExecJS provides the lowest common denominator interface to any JavaScript runtime, so modules are not available and timers are not defined because not all runtimes guarantee a full JavaScript event loop. The README says you should only count on ES3 features being available.
Is ExecJS safe for running untrusted scripts?
No. The README states that ExecJS should not be used for any security related sandboxing, because runtimes are automatically detected and each has different sandboxing properties. It advises against using ExecJS.eval on inputs you would not feel comfortable Ruby eval-ing.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rails-execjs)Was this useful?