Mapping C Error Codes to JavaScript Errors
This page answers one task: a C library compiled to WebAssembly reports failure the C way — a negative return value, sometimes with a message available through a separate call — and JavaScript callers should instead get a thrown error with a type, a code and a readable message.
Prerequisites
- [ ] A C or C++ module built with Emscripten or wasi-sdk whose functions return integer status codes.
- [ ] The ability to add a small amount of C to the module (an error-message accessor), or at least a list of the library’s codes.
- [ ] A JavaScript wrapper module through which callers reach the exports, as in designing a promise-based API around a Wasm module.
Why codes should stop at the wrapper
C libraries signal errors through return values because C has no exceptions: 0 for success, a negative number for failure, maybe a global
errno or a lib_last_error() function for detail. Compiled to WebAssembly, that convention crosses the boundary unchanged — the export returns an
i32, and JavaScript gets a number. Left alone, every caller has to remember to check it, know which numbers mean what, and decide how to report
them. Callers forget, and a failed decode silently becomes an empty image.
JavaScript’s convention is the opposite: failure throws, and the caller handles it in a catch or lets it propagate. The wrapper is where one
convention turns into the other. Every export call passes through a single helper that checks the return value, looks up what it means, fetches any
message the library has, and throws an error object of the right class. Callers then never see a raw code, and a forgotten check is impossible because
there is nothing to check.
Step 1 — make the codes available to both sides
Define the codes once, in the C header, and generate or hand-copy the JavaScript table from it. A header such as this:
// imglib_errors.h
#define IMG_OK 0
#define IMG_EINVAL -1 /* bad argument */
#define IMG_ENOMEM -2 /* allocation failed */
#define IMG_EFORMAT -3 /* input is not a supported image */
#define IMG_ETRUNC -4 /* input ended early */
#define IMG_ELIMIT -5 /* image exceeds configured limits */
maps to a JavaScript table that names each code and picks the error class it should become:
// errors.js
export const CODES = {
[-1]: ["EINVAL", RangeError],
[-2]: ["ENOMEM", OutOfMemoryError],
[-3]: ["EFORMAT", FormatError],
[-4]: ["ETRUNC", FormatError],
[-5]: ["ELIMIT", LimitError],
};
For a library with dozens of codes, generate the table in the build — a ten-line script that parses the #define lines keeps the two sides from
drifting apart when the library adds a code.
Step 2 — expose the library’s error message
Codes say what kind of failure happened; a message says which one. Many C libraries keep a per-context or thread-local error string. Export an accessor that returns a pointer to it:
#include <emscripten.h>
static char last_error[256];
void img_set_error(const char *msg) { /* called inside the library where it fails */
strncpy(last_error, msg, sizeof last_error - 1);
}
EMSCRIPTEN_KEEPALIVE const char *img_last_error(void) { return last_error; }
Reading it from JavaScript is a pointer-to-string conversion — UTF8ToString in Emscripten glue, or a TextDecoder over a subarray of memory for
hand-written glue. Copy the message out immediately: the next failing call overwrites the buffer.
Step 3 — check every call in one helper
export class ImgError extends Error {
constructor(message, code, name) { super(message); this.code = code; this.codeName = name; }
}
export class FormatError extends ImgError { name = "FormatError"; }
export class LimitError extends ImgError { name = "LimitError"; }
export class OutOfMemoryError extends ImgError { name = "OutOfMemoryError"; }
function check(rc) {
if (rc >= 0) return rc; // success, possibly a count or handle
const [codeName, Cls] = CODES[rc] ?? ["EUNKNOWN", ImgError];
const detail = Module.UTF8ToString(Module._img_last_error());
const err = Cls === RangeError ? new RangeError(detail) : new Cls(detail || codeName, rc, codeName);
err.code ??= rc;
throw err;
}
export function decode(bytes) {
const ptr = copyIn(bytes);
try {
const handle = check(Module._img_decode(ptr, bytes.length));
return readImage(handle);
} finally {
Module._free(ptr); // free on success and on failure
}
}
The finally matters as much as the check: buffers passed into a failing call must still be freed, or every error leaks memory. Unknown codes fall back
to the base class with the raw number preserved, so a new code in the library produces a usable error rather than a crash in the wrapper.
Step 4 — let callers branch on the class
Callers now handle failures with ordinary JavaScript:
try {
const img = await decode(bytes);
show(img);
} catch (err) {
if (err instanceof FormatError) toast("That file isn't an image we can read.");
else if (err instanceof LimitError) toast("That image is too large.");
else throw err; // unexpected: let it reach the error tracker
}
Choosing classes for the categories a caller acts on — rather than one class per C code — keeps the surface small. A user-facing app usually needs three or four distinctions: bad input, too large, out of memory, and everything else. The code stays on the error object for logs and support tickets.
Step 5 — distinguish codes from traps
A returned code means the library detected the problem and returned cleanly; the instance is fine and can be used again. A trap — an out-of-bounds
access, an abort(), a failed assertion — is different: the call never returned, and the module may be left mid-update. Keep the two apart in the
wrapper. Codes become the domain errors above; a WebAssembly.RuntimeError should become a separate “engine crashed” error and trigger
re-instantiation, as covered in
recovering a module after a trap.
Emscripten’s abort() also throws, with a message prefixed Aborted(, which the wrapper can recognise in the same place.
Handling errno from libc calls
Ported code often reports failure through libc: fopen returns NULL and sets errno to ENOENT; read returns -1 and sets EIO. In
Emscripten and wasi-libc, errno is an ordinary variable in linear memory, so a JavaScript wrapper cannot read it directly without help. Export a
one-line accessor — int lib_errno(void) { return errno; } — or, better, have the C wrapper function translate errno into the library’s own code
before returning, so JavaScript sees one consistent scheme. The numeric values of errno constants differ between WASI and Emscripten’s musl-based libc,
so never hard-code them in JavaScript; translate inside C, where the right header is in scope, and pass only the library’s own codes across. With
strerror(errno) copied into the last-error buffer, the JavaScript message even carries the familiar “No such file or directory” text.
Expected output
Decoding a truncated PNG throws FormatError: unexpected end of IDAT stream with err.code === -4 and err.codeName === "ETRUNC"; the next call to
decode with a valid file succeeds on the same instance, and heap usage is unchanged after a thousand failing calls.
Gotchas
- Reading the message after another call. The last-error buffer is overwritten. Copy it out before calling anything else.
- Forgetting to free on failure. Use
try … finallyaround every call that takes an allocated buffer. - Treating positive returns as errors. Many functions return counts or handles on success. Check
rc < 0, notrc !== 0. - Duplicated code tables drifting. Generate the JavaScript table from the C header in the build.
- Hard-coding errno values. They differ between libcs. Translate inside C.
Performance note
The check helper costs one comparison on success, so the happy path is unaffected. On failure, reading the message and building the error object cost
about 4 µs in Chrome — negligible next to the decode attempt that failed. The finally free added nothing measurable.
Frequently Asked Questions
Should every C code get its own JavaScript class? No. Group them by what the caller does differently, and keep the precise code as a property.
Can Emscripten throw JavaScript exceptions from C directly?
Yes, through EM_JS or emscripten_run_script, but throwing out of the middle of C skips its cleanup. Returning a code and throwing in the wrapper is
safer.
What about C++ exceptions?
With -fwasm-exceptions they can propagate as WebAssembly.Exception; catching them in a C++ shim and returning codes keeps the boundary simpler.
Should the wrapper log errors as well as throw them? No. Throw, and let the caller or the application’s error boundary decide whether a failure is worth logging. Logging in the wrapper produces duplicates.
How do I test the mapping? Call each export with inputs known to fail in each way and assert the class and code, as part of the module’s normal test suite.
What if the library is not thread-safe about its error buffer?
In a threaded build, keep the last-error buffer thread-local (_Thread_local in C11) so one worker’s failure does not overwrite another’s message.
Related
- Returning error codes without exceptions — the code-based design itself.
- Catching Wasm traps in JavaScript — the other kind of failure.
- Catching memory bugs with Emscripten sanitizers — when the error is a bug, not bad input.
- Handling out-of-memory in Wasm — the ENOMEM case in depth.
← Back to Errors & Traps Across the Boundary