Chapter 14

Custom Sections and Debugging

A module can carry sections that engines ignore: custom sections, with id 0. They hold anything a toolchain wants to attach to a module, and in practice that means names for functions and locals, information about the tools that produced the module, and debugging information. This chapter covers the format, the name section that every compiler should emit, and how debugging works in a browser.

#The format14.1

A custom section is a section with id 0 whose contents start with a name, the section's own name, followed by whatever bytes its producer likes:

00            section id: custom
size          u32
name          a name: the custom section's name
payload       the rest of the section

Custom sections can appear anywhere, any number of times, even between known sections. An engine must accept a module whose custom sections it does not understand, so they never affect whether a module validates or how it runs, with one narrow exception: an engine may report a malformed name section as a warning.

Here is a module with a custom section added by hand, using the encoder helpers from the last chapter:

def uleb(n):
    out = bytearray()
    while True:
        byte, n = n & 0x7f, n >> 7
        out.append(byte | (0x80 if n else 0))
        if not n:
            return bytes(out)

def name(s):
    b = s.encode("utf-8")
    return uleb(len(b)) + b

def section(id, contents):
    return bytes([id]) + uleb(len(contents)) + contents
import subprocess
wasm = b"\x00asm\x01\x00\x00\x00" + section(0, name("build-info") + b"engine-v3 2026-11-01")
open("custom.wasm", "wb").write(wasm)
print(subprocess.run(["wasm-objdump", "-x", "custom.wasm"], capture_output=True, text=True).stdout)
custom.wasm:    file format wasm 0x1

Section Details:

Custom:
 - name: "build-info"

JavaScript can read custom sections from a compiled module, which makes them a convenient place to put metadata a game engine wants to read before instantiating a module, such as a version, or a list of assets a script needs:

wasm = b"\x00asm\x01\x00\x00\x00" + section(0, name("build-info") + b"engine-v3 2026-11-01")
open("custom.wasm", "wb").write(wasm)
import fs from 'node:fs';

const module = new WebAssembly.Module(fs.readFileSync('custom.wasm'));
for (const buf of WebAssembly.Module.customSections(module, 'build-info'))
  console.log(new TextDecoder().decode(buf));
$PYTHON make.py
node read.mjs
engine-v3 2026-11-01

#The name section14.2

The custom section named name maps indices to names, for tools to show. Without it, a stack trace or a profile shows functions as numbers; with it, they have the names your compiler chose. It costs nothing at runtime, and it is by far the most useful custom section, so a compiler should emit it at least in development builds.

The payload is a sequence of subsections, each a one-byte id, a size, and contents. The standard ones are:

Id Subsection Contents
0 module the module's name
1 function a name map: a vector of (function index, name) pairs
2 local for each function, a name map of its locals

Later additions (from the extended name section proposal) give names to labels (3), types (4), tables (5), memories (6), globals (7), element segments (8), data segments (9), struct fields (10) and tags (11). Tools support these unevenly, but they cost nothing to emit.

A name map must be sorted by index, and each subsection may appear at most once, in order of id.

The wat crate's assembler writes a name section with every $name in the text, which is why wasm-objdump has been able to show names throughout this book:

(module $physics
  (func $integrate (param $pos f32) (param $vel f32) (param $dt f32) (result f32)
    (f32.add (local.get $pos) (f32.mul (local.get $vel) (local.get $dt))))
  (func $step (export "step") (result f32)
    (call $integrate (f32.const 0) (f32.const 2) (f32.const 0.5))))
wasm-objdump -x -j name named.wasm | tail -n +5
Section Details:

Custom:
 - name: "name"
 - module <physics>
 - func[0] <integrate>
 - func[1] <step>
 - func[0] local[0] <pos>
 - func[0] local[1] <vel>
 - func[0] local[2] <dt>

#Names in stack traces14.3

When a trap or an exception passes through WebAssembly code, the JavaScript Error it produces has a stack trace with a line for each WebAssembly frame. Here is a trap three calls deep, in a module with names:

(module
  (func $inner (unreachable))
  (func $middle (call $inner))
  (func $outer (export "main") (call $middle)))
import fs from 'node:fs';

const file = process.argv[2];
const { instance } = await WebAssembly.instantiate(fs.readFileSync(file));
try {
  instance.exports.main();
} catch (e) {
  const frames = e.stack.split('\n').filter((l) => l.includes('wasm://'));
  // the URL's hash depends on the module's bytes, so it is left out here
  for (const f of frames) console.log(f.trim().replace(/wasm:\/\/wasm\/[0-9a-f]+/, 'wasm://wasm/…'));
}
node trace.mjs deep.wasm
at inner (wasm://wasm/…:wasm-function[0]:0x23)
at middle (wasm://wasm/…:wasm-function[1]:0x27)
at outer (wasm://wasm/…:wasm-function[2]:0x2c)

Each frame shows the function's name, its index, and the byte offset of the instruction that was executing, from the start of the module. And here is the same module without its name section, removed by wasm-strip, which strips every custom section:

wasm-strip deep.wasm
node trace.mjs deep.wasm
at wasm://wasm/…:wasm-function[0]:0x23
at wasm://wasm/…:wasm-function[1]:0x27
at wasm://wasm/…:wasm-function[2]:0x2c

Without names, each frame says only which function index it was in. You can look those up yourself in a build that kept the names, provided the function indices are the same in both, which they are if stripping is the only difference. That is the usual approach for release builds: ship a stripped module, keep the unstripped one, and translate stack traces from the field after the fact.

The offsets are the same in both, because an offset counts from the start of the module, not the function, and the name section comes after the code. Toolchains put it last for exactly this reason: adding or removing names does not move any code.

#Producers and features14.4

Two other custom sections are common in the wild:

#Source maps and DWARF14.5

A name section tells a debugger which function it is in. To show the source line, it needs a mapping from byte offsets in the code to positions in the source. There are two standard ways to provide one.

Source maps are the format JavaScript minifiers use: a JSON file mapping generated positions to source positions. For WebAssembly, the "generated position" is a byte offset in the code section. A module points at its map with a custom section named sourceMappingURL, whose payload is the map's URL as a name. Chrome, Firefox and Safari all support this, and it is the simplest option for a new compiler: you emit one JSON file alongside the module, and every browser's debugger can then step through your source, line by line. Source maps cannot describe variables, types or scopes, only positions.

DWARF is the debugging format native compilers use, and LLVM writes it into WebAssembly as custom sections named .debug_info, .debug_line and so on, exactly as it would in an ELF file, with code addresses replaced by code-section offsets. It describes everything: lines, variables, types, scopes and inlining. Chrome supports it through a DevTools extension for C and C++, and can show local variables and evaluate expressions in source terms. DWARF is far more work to produce than a source map, and makes modules much bigger, so it usually lives in a separate file named by a custom section called external_debug_info.

#Debugging in the browser14.6

In Chrome or Firefox, a module appears in the debugger's source list under wasm://. Without a source map, the debugger shows the disassembled text format, and lets you set breakpoints on instructions, step, and inspect locals, globals, the operand stack and linear memory. With names, everything has the names your compiler gave it. That alone is enough to debug a compiler's output: put a breakpoint where something goes wrong, and compare what the machine is doing with what your compiler meant it to do.

Two more facilities are worth knowing about:

#What you have seen14.7

Custom sections, id 0, carry a name and arbitrary bytes, and engines ignore them. The name section maps function, local and other indices to names, and makes stack traces, profiles and the debugger readable. Source maps, pointed to by a sourceMappingURL section, give browsers line-level debugging cheaply; DWARF gives full source-level debugging at greater cost. Stripping custom sections for release, and keeping an unstripped copy, is the usual practice.

That ends Part IV. Part V turns to the JavaScript side of the boundary.