# Embedding JavaScript in a Go process with otto

> Otto is a JavaScript parser and interpreter written natively in Go, so ES5 script text can run inside a Go process without a second runtime beside it. Its limits are stated plainly: no ES6 features, no regex lookahead, and a strict mode that parses and does nothing.

**robertkrimen/otto** — A JavaScript interpreter in Go (golang)

- Repository: https://github.com/robertkrimen/otto
- Website: http://godoc.org/github.com/robertkrimen/otto
- Stars: 8,454 · Forks: 605
- Language: Go
- License: MIT
- Published: 2026-10-14 · Updated: 2026-10-14 · Language: en
- Canonical page: https://hysenlabs.com/projects/robertkrimen-otto

## Embedding a JavaScript VM inside a Go process

Otto is a JavaScript parser and interpreter written natively in Go, which lets a Go service evaluate script text in process instead of shipping a second runtime next to it. A VM is created with otto.New() and fed source through vm.Run, so the whole surface is one import and two calls:

```go
vm := otto.New()
vm.Run(`
    abc = 2 + 2;
    console.log("The value of abc is " + abc); // 4
`)
```

Values do not cross the boundary as Go primitives. vm.Get("abc") returns an otto.Value, and value.ToInteger() is the conversion step, so the type you want comes from a method call rather than from an assertion in your own code. That wrapper is the price of the design: every value out, every value in, goes through one interface. The root of the repository is arranged the same way, one file per ECMAScript built-in, running from builtin_array.go and builtin_boolean.go through builtin_date.go, builtin_math.go, builtin_json.go and builtin_regexp.go, with builtin.go and locale.go alongside. Everything described here lives in a single MIT licensed module.

## Go functions enter the VM as otto.Value returns

The boundary runs the other direction as well. vm.Set("sayHello", func(call otto.FunctionCall) otto.Value) publishes a Go func as a JavaScript global, and every such function must return an otto.Value. Returning the empty otto.Value{} means returning nothing, which is why calling sayHello() with no argument still prints Hello, undefined: call.Argument(0).String() has no argument to read and falls back to that text. A function that must answer converts into the VM with vm.ToValue, exactly as twoPlus does before returning 2 plus its integer argument, so twoPlus(2.0) evaluates to 4. Notice that this sample discards the conversion error, writing right, _ := call.Argument(0).ToInteger(), so nothing in that snippet turns a failed conversion into a JavaScript exception. Plain data behaves identically: vm.Set("xyzzy", "Nothing happens.") hands the script a string of 16 characters, and vm.Run("xyzzy.length") comes back as an int64 of 16.

## A failed run reports an error and an undefined value

vm.Run returns two results, and the documentation treats them as separate facts rather than one failure signal. Its error sample calls vm.Run("abcdefghijlmnopqrstuvwxyz.length"), receives err, and the comment identifies it as a ReferenceError for an identifier nobody defined. The following comment states that when there is an error, value.IsUndefined() is true, so a caller who ignores err still holds a well-formed otto.Value instead of a nil. That shape is worth copying into your own wrapper: read err for the message, and only feed the undefined value onward when a failed script is something you have decided to tolerate. Elsewhere the same excerpt declares ErrVersion as errors.New("version mismatch") and then stops, without naming the call that returns it, so treat that identifier as an API surface you will have to discover from the source.

## The standalone parser yields an AST without a VM

Parsing is separable from execution. A parser package sits beside the interpreter, and its stated purpose is building only an AST. One call does the work, parser.ParseFile(nil, filename, src, 0), where the first argument is the file set, a filename is optional, and the trailing 0 is the mode. The signature returns a *ast.Program and, in the wording of the example, an ErrorList, so a caller can receive a partial tree together with a list of problems instead of a single all-or-nothing failure. That combination is what makes the package usable as a front end that never starts a VM: parse once, walk the nodes, decide for yourself what is an error. The node types live under the ast/ directory, while the top level also carries cmpl.go, cmpl_parse.go, cmpl_evaluate.go, cmpl_evaluate_expression.go and cmpl_evaluate_statement.go, files named for parsing and evaluating source. A DESIGN.markdown file sits beside them, and the README never points at it, so the reasoning behind the layout is not summarised anywhere in the quick start.

## Running example.js through the otto command

The command ships inside the module rather than beside it, so the install path carries a second otto segment:

```shell
go install github.com/robertkrimen/otto/otto@latest
```

The @latest suffix asks for the head of the default branch, master, rather than for a tag, so the binary on your machine is whatever that branch held on the day you built it. Running source is the next step, and two routes are offered: type source on stdin, or hand the command a filename.

```shell
otto example.js
```

The require block in go.mod is worth reading before you pin anything. gopkg.in/readline.v1 backs the interactive stdin path, gopkg.in/sourcemap.v1 v1.0.5 and gopkg.in/yaml.v3 v3.0.1 come in with it, and golang.org/x/text v0.4.0 sits behind the locale handling in locale.go. The same direct block also carries github.com/stretchr/testify v1.8.1, so a vendored or audited build pulls the assertion library into a program that only asked for a parser.

## Interrupt channels, not deadlines, stop runaway loops

There is no CPU budget parameter on vm.Run and no deadline argument. The documented control is vm.Interrupt, a channel of func() that execution consults while it runs. The sample assigns `vm.Interrupt = make(chan func(), 1)` and comments that the buffer prevents blocking, then starts a goroutine waiting on a 2 second timer that pushes a func() whose only statement is panic(halt). The script side recovers that panic, compares the recovered value against a package level error variable also named halt, prints the elapsed time, and repanics anything that does not match. That equality check runs on interface values, so a genuine bug inside a Go function reaches your caller instead of being reported as a timeout. One more detail is easy to miss: the goroutine closes the channel once the timer fires, so every watchdog is single shot, and the sample compensates by constructing a fresh VM inside runUnsafe for each piece of code it wants to time.

## re2 drops lookahead and backreferences at parse time

Regular expressions are rewritten by parser.TransformRegExp into Go syntax and handed to re2, and re2 has no backtracking. Three ES5 constructs therefore fail while the pattern is parsed rather than when it is matched:

```plaintext
(?=)  // Lookahead (positive), currently a parsing error
(?!)  // Lookahead (backhead), currently a parsing error
\1    // Backreference (\1, \2, \3, ...), currently a parsing error
```

Because the failure lands at parse time, a script carrying one of those patterns cannot be loaded at all, even when the branch that uses it never runs. Character classes drift in quieter ways: re2 defines \s as the class [\t\n\f\r ] with a literal space at the end, while the JavaScript definition also includes \v and the Unicode "Separator, Space" category, so an anchored class can accept different text under otto than it does elsewhere. The cause is structural rather than a missing feature list, since the translation step assumes patterns that need no backtracking.

## otto targets ES5 yet leaves part of ECMA5 unimplemented

Three limits set the ceiling. First, otto targets ES5, and some ES6 features, Typed Arrays among them, are not supported. Second, the regular expression engine is described as not fully compatible with the ECMA5 specification, so on the very edition otto claims to target, a documented part is missing; that is the line most readers skim past, because it sits in the same list that reads like ordinary version lag. Third, `use strict` will parse but does nothing, which is worse than a parse error, since a script written for strict semantics loads cleanly and then runs without the checks it asked for. Timing functions are absent for a different reason: setTimeout and setInterval are not part of the ECMA-262 specification, they belong to the window object in a browser, and providing them means wrapping otto in an event loop, which the documentation hands to a separate project, natto. The last push was on 2026-10-01 while the newest tag, v0.5.1, dates from 2024-11-05, so the tree is being worked on while the tagged line stays where it is.

## Conclusion

Otto is a deliberate fit for Go services that must execute short ES5 scripts in process, where operating a second runtime is not something you want, and a poor fit for a build pipeline that meets ES6 syntax. Before adopting it, check three things in your own script set: whether anything relies on Typed Arrays or other ES6 features, whether one regular expression uses lookahead or a backreference, and whether the missing strict mode changes code you plan to feed it. Pin a tag rather than tracking the 2026-10-01 tree, and read DESIGN.markdown before relying on internals.

## FAQ

### Is otto still maintained, and which Go version does it require?

The last push was on 2026-10-01 and the newest tag is v0.5.1 from 2024-11-05, so commits are landing on the default branch while tagged releases stay behind. The go.mod file declares go 1.22, which is the minimum toolchain the module asks for.

### How do I stop an infinite loop inside otto?

Assign vm.Interrupt a channel of func() and push a function that panics when the code runs too long. The sample uses a buffer of 1 so the send does not block, recovers the panic, compares it with a halt error and repanics anything else.

### Why does a regular expression with lookahead fail in otto?

parser.TransformRegExp rewrites the pattern into Go syntax and re2 has no backtracking, so (?=) and (?!) are reported as parsing errors, as are backreferences such as \1. The failure happens when the pattern is parsed, not when it matches.

### Where do setTimeout and setInterval come from in otto?

They are not part of the ECMA-262 specification and normally belong to the window object in a browser, so otto does not ship them. Supplying equivalents means wrapping the interpreter in an event loop, which the README points at through a separate project called natto.

### What does the version mismatch error in otto mean?

The exported identifier ErrVersion is declared as errors.New("version mismatch"), and the usage excerpt ends there. The README does not document which call returns it, so read the source before matching on that value.

## Sources

- [License: MIT](https://github.com/robertkrimen/otto/blob/master/LICENSE)
- [Project website](http://godoc.org/github.com/robertkrimen/otto)
- [README](https://github.com/robertkrimen/otto/blob/master/README.md)
- [Releases](https://github.com/robertkrimen/otto/releases)
- [robertkrimen/otto on GitHub](https://github.com/robertkrimen/otto)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/robertkrimen-otto
