syscall/js: Go in the Browser
The syscall/js package lets Go WASM talk to JavaScript: DOM nodes, fetch, timers, and callbacks from JS into Go.
Search across all documentation pages
The syscall/js package lets Go WASM talk to JavaScript: DOM nodes, fetch, timers, and callbacks from JS into Go.
It is the distinguishing piece of the GOOS=js GOARCH=wasm target.
Keep it behind build tags so server-side WASI modules never import browser-only syscalls.
Browser Go WASM compiles with GOOS=js and GOARCH=wasm.
The host page loads wasm_exec.js plus your main.wasm.
Go functions registered with js.FuncOf receive JS events; js.Global() reaches browser globals like document and window.
Because main would otherwise exit, long-lived UIs block with select {}.
Quick-reference recipe card - copy-paste ready.
//go:build js && wasm
package main
import (
"syscall/js"
)
func main() {
doc := js.Global().Get("document")
btn := doc.Call("createElement", "button")
btn.Set("textContent", "Click me")
btn.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
js.Global().Get("console").Call("log", "clicked from Go")
return nil
}))
doc.Get("body").Call("appendChild", btn)
select {}
}When to reach for this:
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Go WASM Demo</title>
<script src="wasm_exec.js"></script>
</head>
<body>
<div id="out"></div>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then((result) => {
go.run(result.instance);
});
</script>
</body>
</html>main.go:
//go:build js && wasm
package main
import (
"strconv"
"syscall/js"
)
func main() {
counter := 0
doc := js.Global().Get("document")
out := doc.Call("getElementById", "out")
btn := doc.Call("createElement", "button")
btn.Set("innerText", "Increment")
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
counter++
out.Set("innerText", "count="+strconv.Itoa(counter))
return nil
})
btn.Call("addEventListener", "click", handler)
out.Call("appendChild", btn)
select {}
}Build and serve:
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
GOOS=js GOARCH=wasm go build -o main.wasm .
python3 -m http.server 8080What this demonstrates:
js.FuncOf wraps Go closures as JS-callable functions.js.Value method calls, not cgo.select {} prevents main from returning and tearing down the runtime.wasm_exec.js supplies imports the Go WASM module requires.wasm_exec.js.js.Value is an opaque handle to a JS value; Get, Set, and Call marshal across the boundary.js.Value handles; release long-lived callbacks with js.Func.Release() when removing listeners.| File tag | Compiled when | Role |
|---|---|---|
//go:build js && wasm | Browser target | DOM, syscall/js entry |
//go:build !js || !wasm | Native + WASI | Shared logic without JS imports |
| default (no tag) | All non-tagged targets | Portable packages |
// Fetch example (host must provide fetch in globals)
promise := js.Global().Call("fetch", "/api/data")
// Then chain .then with js.FuncOf handlers - mirror JS promise style// Release callbacks registered on long-lived pages to avoid leaks.
var fn js.Func
fn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
fn.Release()
return nil
})Forgetting select {} - main returns, the runtime exits, and callbacks never fire. Fix: block on select {} or a channel fed by shutdown logic.
Importing syscall/js in WASI builds - Breaks GOOS=wasip1 compilation or bloats headless modules. Fix: strict js && wasm file tags.
Serving without correct MIME type - Some static servers serve .wasm as application/octet-stream. Fix: use application/wasm or instantiateStreaming fails; configure server MIME map.
Leaking js.Func callbacks - Re-registering listeners without Release retains Go closures. Fix: call Release() when removing DOM nodes or navigating away.
Expecting full stdlib networking - Browser security shapes net/http; many patterns need fetch via JS. Fix: wrap fetch or keep network code in WASI workers.
Large bundle sizes - js/wasm Go binaries are multi-megabyte. Fix: measure before committing; consider TinyGo or host compute off-thread.
| Alternative | Use When | Don't Use When |
|---|---|---|
wasip1/wasm in browser via WASI polyfill | Compute-heavy, no direct DOM | You need fine-grained DOM control |
| TypeScript/React UI + WASM plugin | Production UX with Go compute core | Team wants pure Go UI code |
TinyGo wasm target | Smaller browser WASM | You need full reflect/generics libs |
| WebAssembly-GC languages (future hosts) | Host supports GC proposal | You must ship today on all browsers |
Yes for the js/wasm target, but treat it as a specialized bridge.
Default new work to wasip1 unless DOM access is explicit.
Copy from $(go env GOROOT)/misc/wasm/wasm_exec.js.
Version it with your Go toolchain so imports stay matched.
Yes. Tag every file that imports syscall/js with js && wasm, or place them in a browser-only package.
Export functions with js.Global().Set("goDoThing", js.FuncOf(...)) before blocking.
Read values from JS through args []js.Value.
Not on js/wasm builds.
Keep concurrency tests on linux/amd64 packages shared with WASM logic.
Yes - output routes to the browser console via the JS runtime shim.
Prefer structured logging for real apps.
Use js.CopyBytesToJS and js.CopyBytesToGo helpers to move data across the boundary without extra copies where possible.
You can run WASM in Web Workers with a custom wasm_exec.js loader, but DOM APIs are not available in workers.
Architect accordingly.
TinyGo supports a subset of WASM targets; verify its JS interop docs for your version.
API shapes differ from gc toolchain behavior.
No. Headless WASM hosts do not provide JS shims.
Split packages and enforce with lint rules or go vet build tag checks.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026