JavaScript syscall/js Utilities for Go WASM Development: A Complete Guide
The aperturerobotics/util repository provides lightweight Go packages that wrap browser JavaScript APIs via syscall/js, enabling HTTP requests and stream handling in WebAssembly without importing net/http.
When compiling Go to WebAssembly (WASM), the standard net/http package is unavailable, forcing developers to bridge Go and JavaScript manually using the syscall/js package. The aperturerobotics/util repository solves this with a focused set of JavaScript syscall/js utilities that expose the browser's native Fetch API and ReadableStream objects to Go code. These wrappers handle the complex promise-to-channel synchronization and byte-array copying required for seamless WASM networking.
Core syscall/js Utilities in aperturerobotics/util
The repository's js/ directory contains four primary utilities that work together to provide HTTP client functionality in WASM environments.
Fetch API Wrapper (js/fetch)
The js/fetch package provides a thin, idiomatic Go interface to the browser's fetch() function. Located in [js/fetch/fetch.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/fetch.go), the fetch.Fetch() function constructs request options, invokes js.Global().Call("fetch", ...), and translates the JavaScript Response object into a Go *Response struct implementing io.ReadCloser.
The wrapper handles promise resolution by attaching then and catch callbacks using js.FuncOf, pushing results onto a Go channel to bridge JavaScript's asynchronous model with Go's synchronous execution flow.
HTTP Header Management
The [js/fetch/header.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/header.go) file defines a Header type that mirrors the standard net/http.Header interface while working with JavaScript objects. It provides methods including Add(), Set(), Get(), Clone(), and Write(), along with canonicalization logic to normalize header names (e.g., converting "content-type" to "Content-Type").
This allows WASM applications to construct and parse HTTP headers using familiar Go patterns while internally converting to JavaScript-compatible objects via syscall/js.
Fetch Configuration Enums
The [js/fetch/enums.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/enums.go) file exports string constants for all Fetch API configuration options. These include HTTP methods (MethodGet, MethodPost, etc.), cache modes, credentials modes, request modes, redirect behaviors, and referrer policies.
Using these typed constants prevents runtime errors from typos in string literals and provides IDE autocomplete for valid Fetch API values.
ReadableStream Implementation (js/readable-stream)
The [js/readable-stream/stream.go](https://github.com/aperturerobotics/util/blob/master/js/readable-stream/stream.go) package implements io.ReadCloser on top of JavaScript ReadableStream objects. The NewReadableStream() function accepts a js.Value representing a stream and returns a Go type that can be used with standard library functions like io.Copy() or bufio.NewReader().
Internally, the wrapper manages the asynchronous reader.read() promise, buffers incoming Uint8Array chunks using js.CopyBytesToGo, and implements a "prefixedReader" shim to handle any bytes peeked during stream validation. It also properly closes the stream and releases JavaScript resources when Close() is called.
How the Utilities Work Together
These JavaScript syscall/js utilities combine to provide a complete HTTP client workflow:
-
Request Construction: Create
fetch.OptsembeddingCommonOptsfor cache, credentials, and mode settings. Attach afetch.Headermap for metadata and provide anio.Readerbody if needed. -
Promise Handling:
fetch.Fetchconverts the options to a JavaScript object usingjs.ValueOf, creates anAbortControllerif cancellation is requested, and callsjs.Global().Call("fetch", url, mapOpts). It attaches callbacks viajs.FuncOfthat push results onto a Go channel. -
Response Parsing: The success callback extracts status codes, headers (using the JS iterator protocol on
response.headers.entries()), and the response URL from the JavaScriptResponseobject. -
Body Streaming: If the response contains a non-null
body, the wrapper creates astream.NewReadableStream(body)which implementsio.ReadCloser. This allows standard Go I/O operations on the streaming response body. -
Cancellation: When an
opts.Signal(context.Context) is provided, the wrapper creates a JavaScriptAbortController, attaches its signal to the fetch request, and usescontext.AfterFuncto callcontroller.Call("abort")if the context is cancelled.
Practical Code Examples
Simple GET Request with Timeout
This example demonstrates a basic fetch with context-based cancellation, using the js/fetch package to avoid the unavailable net/http client in WASM.
package main
import (
"context"
"fmt"
"io"
"time"
"github.com/aperturerobotics/util/js/fetch"
)
func main() {
// Context with timeout – aborts the fetch if it takes too long.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Build options – we only need the cancellation signal here.
opts := &fetch.Opts{
Method: fetch.MethodGet,
Signal: ctx,
}
// Perform the request.
resp, err := fetch.Fetch("https://httpbin.org/get", opts)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Read the whole body.
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %d\nBody: %s\n", resp.StatusCode, body)
}
Key implementation details from [js/fetch/fetch.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/fetch.go): fetch.Fetch invokes js.Global().Call("fetch", ...) and translates the JavaScript Response into a Go *Response implementing io.ReadCloser.
POST Request with JSON Payload
This example shows how to send structured data using the Header type for content negotiation and the Body field for the request payload.
package main
import (
"context"
"encoding/json"
"io"
"strings"
"time"
"github.com/aperturerobotics/util/js/fetch"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
payload := `{"msg":"hello"}`
opts := &fetch.Opts{
Method: fetch.MethodPost,
Signal: ctx,
Body: strings.NewReader(payload),
Header: fetch.Header{
"Content-Type": []string{"application/json"},
},
}
resp, err := fetch.Fetch("https://httpbin.org/post", opts)
if err != nil {
panic(err)
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
var out map[string]any
_ = json.Unmarshal(b, &out)
// `out["json"]` now holds the parsed request body.
}
Implementation highlights from [js/fetch/header.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/header.go): The Header type automatically canonicalizes keys (e.g., "content-type" to "Content-Type") and provides the standard Add, Set, and Get methods familiar from net/http.
Direct ReadableStream Usage
When integrating with existing JavaScript libraries that return ReadableStream objects, the js/readable-stream package provides direct conversion to Go's io.ReadCloser.
import (
"github.com/aperturerobotics/util/js/readable-stream"
"syscall/js"
)
func streamFromJS(jsStream js.Value) io.ReadCloser {
return stream.NewReadableStream(jsStream)
}
Now you can use standard Go I/O operations:
reader := stream.NewReadableStream(jsStream)
bufReader := bufio.NewReader(reader)
line, err := bufReader.ReadString('\n')
From [js/readable-stream/stream.go](https://github.com/aperturerobotics/util/blob/master/js/readable-stream/stream.go): The implementation manages the asynchronous reader.read() promise, buffers Uint8Array chunks using js.CopyBytesToGo, and includes a "prefixedReader" shim to handle bytes peeked during validation.
Key Source Files and Architecture
The JavaScript syscall/js utilities in aperturerobotics/util are organized under the js/ directory with clear separation of concerns:
| File | Purpose |
|---|---|
[js/fetch/fetch.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/fetch.go) |
Core fetch wrapper – builds options, invokes the JS Fetch API, handles promises via channels, and translates Response objects. |
[js/fetch/header.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/header.go) |
HTTP Header map implementation with canonicalization, compatible with net/http.Header patterns. |
[js/fetch/enums.go](https://github.com/aperturerobotics/util/blob/master/js/fetch/enums.go) |
Typed constants for Fetch API options (methods, cache modes, credentials, redirects). |
[js/readable-stream/stream.go](https://github.com/aperturerobotics/util/blob/master/js/readable-stream/stream.go) |
io.ReadCloser implementation over JavaScript ReadableStream with async read management. |
[README.md](https://github.com/aperturerobotics/util/blob/master/README.md) |
Repository documentation listing available syscall/js utilities. |
These files work together to provide a native-like HTTP client for Go-WASM that operates entirely within the browser environment, avoiding the unsupported net/http package.
Summary
- The aperturerobotics/util repository provides essential JavaScript syscall/js utilities for Go WebAssembly development, specifically targeting HTTP networking and stream handling.
- The
js/fetchpackage wraps the browser's native Fetch API, offering a Go-idiomatic interface with support for contexts, cancellation, and standard HTTP headers. - The
js/readable-streampackage bridges JavaScript ReadableStream objects to Go'sio.ReadCloser, enabling standard library I/O operations on streaming data. - All utilities avoid the unsupported
net/httppackage, instead usingsyscall/jsto interact directly with browser APIs viajs.Global().Call(),js.FuncOf(), andjs.CopyBytesToGo().
Frequently Asked Questions
How do these utilities differ from using net/http in Go?
Standard Go applications use net/http for HTTP requests, but this package is not available when compiling to WebAssembly because it relies on low-level network primitives that browsers do not expose. The JavaScript syscall/js utilities in aperturerobotics/util instead call the browser's native Fetch API directly via syscall/js, providing equivalent functionality while remaining compatible with WASM targets.
Can I use context cancellation with fetch requests?
Yes. The fetch.Opts struct accepts a Signal field that takes a context.Context. When provided, the wrapper creates a JavaScript AbortController and attaches its signal to the fetch request. If the context is cancelled or times out, the wrapper automatically calls controller.Call("abort"), terminating the in-flight request and returning the context error to Go code.
How does the ReadableStream wrapper handle backpressure?
The js/readable-stream implementation manages backpressure by controlling the JavaScript reader's read() promise lifecycle. It buffers incoming Uint8Array chunks using js.CopyBytesToGo and only requests the next chunk when the Go side consumes the current buffer via Read(). The wrapper also implements a "prefixedReader" pattern to handle any bytes peeked during stream validation, ensuring no data is lost during the transition from JavaScript streams to Go io.Reader semantics.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →