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:

  1. Request Construction: Create fetch.Opts embedding CommonOpts for cache, credentials, and mode settings. Attach a fetch.Header map for metadata and provide an io.Reader body if needed.

  2. Promise Handling: fetch.Fetch converts the options to a JavaScript object using js.ValueOf, creates an AbortController if cancellation is requested, and calls js.Global().Call("fetch", url, mapOpts). It attaches callbacks via js.FuncOf that push results onto a Go channel.

  3. Response Parsing: The success callback extracts status codes, headers (using the JS iterator protocol on response.headers.entries()), and the response URL from the JavaScript Response object.

  4. Body Streaming: If the response contains a non-null body, the wrapper creates a stream.NewReadableStream(body) which implements io.ReadCloser. This allows standard Go I/O operations on the streaming response body.

  5. Cancellation: When an opts.Signal (context.Context) is provided, the wrapper creates a JavaScript AbortController, attaches its signal to the fetch request, and uses context.AfterFunc to call controller.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/fetch package wraps the browser's native Fetch API, offering a Go-idiomatic interface with support for contexts, cancellation, and standard HTTP headers.
  • The js/readable-stream package bridges JavaScript ReadableStream objects to Go's io.ReadCloser, enabling standard library I/O operations on streaming data.
  • All utilities avoid the unsupported net/http package, instead using syscall/js to interact directly with browser APIs via js.Global().Call(), js.FuncOf(), and js.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →