How to Load JavaScript from a String in Go
Run JavaScript source text inside Go with Goja, handle values and errors, call functions, and understand compatibility and security limits.

To load JavaScript from a string in Go, embed a JavaScript runtime and pass the source to its evaluation method. The clearest current example is Goja: create a runtime with goja.New(), call RunString, check the returned error, and use Value.Export() or ExportTo to move the result into Go.
package main
import (
"fmt"
"log"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
value, err := vm.RunString(`2 + 2`)
if err != nil {
log.Fatal(err)
}
fmt.Println(value.Export()) // 4
}
Goja’s README and its package documentation document this runtime-and-RunString flow. The rest of this guide shows how to install it, pass data in and out, call a function defined by the string, handle failures, and decide whether an embedded interpreter fits your application.
1. Add Goja to your Go module
Create a module if you do not already have one, then add Goja as a dependency:
mkdir js-string-runner
cd js-string-runner
go mod init example.com/js-string-runner
go get github.com/dop251/goja
Put the first example in main.go and run it:
go run .
Goja is implemented in pure Go and documents ECMAScript 5.1 support, with much of ES6 still in progress. That means source that depends on newer syntax or browser and Node.js APIs needs to be checked against the Goja version you adopt. An embedded runtime evaluates JavaScript language features; it does not automatically provide window, document, fetch, the filesystem, or Node’s module system.
2. Evaluate a string and convert the result
RunString evaluates source in the runtime’s global context. It returns both a JavaScript Value and an error. Always handle the error before reading or exporting the value.
package main
import (
"fmt"
"log"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
source := `
const prices = [12, 8, 5];
prices.reduce((sum, price) => sum + price, 0);
`
value, err := vm.RunString(source)
if err != nil {
log.Fatalf("JavaScript failed: %v", err)
}
fmt.Printf("JavaScript value: %v\n", value.Export())
}
For simple values, Export() is convenient. Goja also documents ExportTo when you want conversion into a specific Go destination:
package main
import (
"fmt"
"log"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
value, err := vm.RunString(`[1, 2, 3, 4]`)
if err != nil {
log.Fatal(err)
}
var numbers []int
if err := value.ExportTo(&numbers); err != nil {
log.Fatalf("cannot convert result: %v", err)
}
fmt.Println(numbers)
}
3. Pass Go values into JavaScript
Use Runtime.Set to expose a Go value under a JavaScript name. Goja converts ordinary Go values for use by the script; Runtime.ToValue is available when you need to create a JavaScript value explicitly.

package main
import (
"fmt"
"log"
"github.com/dop251/goja"
)
type User struct {
Name string `json:"name"`
Admin bool `json:"admin"`
}
func main() {
vm := goja.New()
vm.Set("user", User{Name: "Mina", Admin: true})
vm.Set("limit", 10)
value, err := vm.RunString(`
user.admin && user.name + " has a limit of " + limit;
`)
if err != nil {
log.Fatal(err)
}
fmt.Println(value.Export())
}
For maps and slices, prefer values with predictable types and document the shape your JavaScript receives. If the script must mutate shared application state, consider passing a copy or a narrow API instead of exposing a large Go object.
4. Define and call a JavaScript function
A common pattern is to load a function from a string once, then call it with different inputs. The Goja README demonstrates retrieving a value from the runtime and using goja.AssertFunction.
package main
import (
"fmt"
"log"
"github.com/dop251/goja"
)
func main() {
vm := goja.New()
_, err := vm.RunString(`
function total(items, taxRate) {
const subtotal = items.reduce((sum, item) => sum + item, 0);
return subtotal * (1 + taxRate);
}
`)
if err != nil {
log.Fatal(err)
}
fnValue := vm.Get("total")
fn, ok := goja.AssertFunction(fnValue)
if !ok {
log.Fatal("total is not a function")
}
result, err := fn(
goja.Undefined(),
vm.ToValue([]int{10, 20, 5}),
vm.ToValue(0.2),
)
if err != nil {
log.Fatalf("function failed: %v", err)
}
fmt.Println(result.Export()) // 42
}
The first argument to the function call is its JavaScript this value. Use a different value when the function relies on a receiver. Calling the function through Goja also gives you a place to validate arguments and translate execution errors into your application’s error model.
5. Load a script from a file or an HTTP response
If your source starts as a file, read it and pass the resulting string to RunString. For remote source, apply strict allowlists, size limits, timeouts, and integrity checks before evaluation.
package main
import (
"fmt"
"log"
"os"
"github.com/dop251/goja"
)
func main() {
source, err := os.ReadFile("rules.js")
if err != nil {
log.Fatal(err)
}
vm := goja.New()
value, err := vm.RunString(string(source))
if err != nil {
log.Fatalf("rules.js failed: %v", err)
}
fmt.Println(value.Export())
}
Do not confuse loading text with loading modules. RunString executes a script in one runtime context; it does not implement npm installation, CommonJS require, or ES module resolution for you.
6. Syntax errors, runtime errors, and interruptions
Both invalid source and failures while executing valid source are reported through the returned error. Keep source labels in your logs so an operator can identify which script failed.
func runScript(vm *goja.Runtime, name, source string) (goja.Value, error) {
value, err := vm.RunString(source)
if err != nil {
return nil, fmt.Errorf("%s: %w", name, err)
}
return value, nil
}
Long-running scripts need an operational limit. Goja documents an interruption mechanism, but an interruption example is not a security guarantee. Treat it as a way to stop work cooperatively, not as proof that hostile code is isolated. For untrusted code, place the interpreter in a separate process or stronger sandbox and restrict CPU, memory, file access, network access, and process lifetime outside the JavaScript engine.
7. Goja versus Otto
| Question | Goja | Otto |
|---|---|---|
| Evaluate source | Runtime.RunString |
VM.Run |
| Result handling | JavaScript Value, then Export or ExportTo |
Returns a value and error |
| Language guidance | README documents ECMAScript 5.1, with ES6 work in progress | Use the project’s documentation and verify the syntax your application needs |
| Isolation | The reviewed documentation does not establish a secure sandbox | |
Otto is a reasonable alternative for basic embedded execution:
package main
import (
"fmt"
"log"
"github.com/robertkrimen/otto"
)
func main() {
vm := otto.New()
value, err := vm.Run(`2 + 2`)
if err != nil {
log.Fatal(err)
}
result, err := value.ToInteger()
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
Choose by checking the JavaScript features you require, the value-exchange APIs you prefer, dependency maintenance, and the isolation boundary your application needs. The sources reviewed here do not provide an apples-to-apples current performance or compatibility ranking.
8. Practical design checklist
- Validate or control the source before evaluation.
- Set a maximum source size and reject unexpectedly large scripts.
- Check the
errorfrom everyRunString, function call, and conversion. - Expose only the Go data and functions the script needs.
- Use a fresh runtime per tenant or trust boundary when state must not leak.
- Reuse a runtime only when shared globals and concurrency rules are understood. A runtime is stateful; do not assume one instance is safe for concurrent use without consulting the version’s documentation.
- Record script name, version, duration, and outcome, while avoiding secrets in source logs.
- Test syntax features against the exact Goja or Otto version in your module.
9. Troubleshooting
“Unexpected token” or syntax errors
Cause: the source uses syntax unsupported by the selected runtime, or the string is malformed. Fix: log a source identifier, validate the script separately, and confirm the engine’s documented language level. Goja’s documented baseline is ECMAScript 5.1 with newer features still in progress.
The result is undefined
Cause: the last statement has no value, or the script defines a function without calling it. Fix: make the final expression explicit, assign a value to a known global, or retrieve and invoke the function with AssertFunction.
“X is not defined”
Cause: embedded runtimes do not automatically provide browser or Node.js globals. Fix: inject a narrow replacement with vm.Set, add the required host API yourself, or use a runtime designed for that environment.
Export conversion fails
Cause: the JavaScript value’s shape or types do not match the Go destination. Fix: inspect value.Export(), use a destination with matching fields and types, and handle conversion errors before using the result.
The process uses too much CPU or memory
Cause: an infinite loop, large allocation, or expensive script. Fix: impose operational limits, interrupt work where supported, cap input size, and move untrusted execution to an isolated process with OS-level resource limits.
State appears in a later request
Cause: a reused runtime retains globals and objects. Fix: create a new runtime for each isolation boundary, or explicitly reset all state and document the reuse policy.
10. Performance, reliability, and cost
Embedding Goja or Otto avoids starting a separate JavaScript process for each evaluation, which can simplify deployment. Actual latency and memory use depend on script size, allocations, conversions, and runtime reuse; the supplied sources do not establish a benchmark. Measure your own workload with representative scripts and concurrency.
For reliability, fail closed when source validation, execution, or conversion returns an error. Keep the JavaScript contract small and versioned. If scripts are supplied by customers, a process boundary and resource quotas are more dependable than treating an in-process interpreter as a security sandbox.
The libraries themselves are dependencies in your Go binary. Your operational cost is therefore mainly compute, memory, storage, and engineering effort. If the actual task is rendering a website rather than evaluating a JavaScript expression, an embedded interpreter is the wrong layer: it does not load a page, run browser layout, or produce a screenshot.
11. Or skip the browser setup
If your Go service ultimately needs a website screenshot, use ScreenshotNeo instead of assembling a browser runtime. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. The API removes cookie and consent banners, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.

See the ScreenshotNeo API documentation for all options. A minimal call from Go can use the standard HTTP client:
package main
import (
"log"
"os"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "https://api.screenshotneo.com/v1/shot", nil)
if err != nil {
log.Fatal(err)
}
query := req.URL.Query()
query.Set("access_key", "YOUR_API_KEY")
query.Set("url", "https://stripe.com")
req.URL.RawQuery = query.Encode()
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
log.Fatalf("ScreenshotNeo returned %s", res.Status)
}
out, err := os.Create("shot.webp")
if err != nil {
log.Fatal(err)
}
defer out.Close()
if _, err := out.ReadFrom(res.Body); err != nil {
log.Fatal(err)
}
}
The same request in cURL is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also supports full-page captures with lazy images, CSS-selector element shots, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
Plans include 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
12. FAQ
Can Go execute JavaScript without Node.js?
Yes. Goja and Otto embed JavaScript interpreters directly in a Go process. They do not automatically provide Node.js modules or browser APIs.
Does RunString return JSON?
No. It returns a JavaScript value. Use Export or ExportTo, then encode the resulting Go value as JSON if your application needs JSON.
Should I use one runtime for every request?
Only when shared state and concurrency are intentional and supported by your chosen version. A fresh runtime is simpler across trust boundaries.
Is Goja a secure sandbox for customer scripts?
The reviewed documentation does not establish that. Use process isolation and OS-level controls for hostile or untrusted code.
Can an embedded runtime take a screenshot of a web page?
No. It evaluates JavaScript source; it does not provide browser rendering. Use a screenshot service such as ScreenshotNeo when the output must be an image or PDF.


