Passing Strings
WebAssembly functions only accept and return numeric types — integers and floats. There is no string type. The standard workaround is to encode string data as bytes in linear memory and pass integer coordinates (a pointer and a length) to identify where those bytes live.
The pattern
Section titled “The pattern”The round-trip from JavaScript string to Wasm and back has five steps:
- Encode the string to a
Uint8Arrayof UTF-8 bytes usingTextEncoder - Write those bytes into a known region of Wasm memory
- Call the Wasm function with the byte offset (pointer) and the byte count (length)
- Wasm does its work and returns the pointer and length of the result bytes
- Decode the result bytes back to a JavaScript string using
TextDecoder
flowchart LR A["JS string\n'Hello'"] --> B["TextEncoder\n-> Uint8Array"] B --> C["mem.buffer\n(write at ptr)"] C --> D["Wasm function\n(ptr, len)"] D --> C C --> E["TextDecoder\n-> JS string"]
Encoding with TextEncoder
Section titled “Encoding with TextEncoder”TextEncoder.encode(str) converts a JavaScript string to a Uint8Array of UTF-8 bytes. Multi-byte characters (emoji, accented letters, CJK) produce more bytes than characters, so always use the byte length for memory operations, not the string length.
Writing bytes into Wasm memory
Section titled “Writing bytes into Wasm memory”Pick an address in Wasm memory that you control (for example, a static region starting at byte 256) and write the encoded bytes there:
const encoded = new TextEncoder().encode(str);const ptr = 256;new Uint8Array(mem.buffer).set(encoded, ptr);Reading bytes back with TextDecoder
Section titled “Reading bytes back with TextDecoder”After calling the Wasm function you get back (ptr, len). Slice the memory view and decode:
const decoded = new TextDecoder().decode( new Uint8Array(mem.buffer, outPtr, outLen));The three-argument Uint8Array constructor creates a view starting at outPtr with outLen elements — no copy, no allocation.
Run it
Section titled “Run it”The Wasm module below exposes an echo function that simply returns the same (ptr, len) pair it receives. The JavaScript side encodes "Hello Wasm", writes it at offset 256, calls echo, then decodes the result.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
Send (ptr, len) through linear memory instead of a string directly | Zero-copy byte read/write, no re-serializing across the host boundary | You have to encode/decode UTF-8 yourself and compute the byte length correctly every time |
| A fixed address for writing strings (manual layout) | Simple, no allocator or extra runtime needed | No GC to warn you when a new string is longer than the reserved space — you must budget the buffer size yourself |
Common mistakes
Section titled “Common mistakes”- Using
str.length(character count) instead ofencoded.length(the real byte count after UTF-8 encoding) when computing the length to pass to Wasm, truncating or mis-decoding multi-byte characters like emoji or Thai text - Writing bytes at a fixed offset (e.g. 256) without leaving room for the string to grow, letting new data overwrite another region still in use
- Reusing a cached typed-array view across calls and forgetting that a
memory.growin between detaches the old view, causing the string to be written or read at the wrong location
💡 Real-world example
FFmpeg.wasm passes filenames and metadata strings through this same
(ptr, len)shape across linear memory to drive a decoder compiled from C, avoiding a slower string-conversion glue layer.