Limitations
Honesty is the product. The static surface is large but not total, and a few behaviors diverge from Node by design. This page is the plain-language version; scriptc coverage on your program is the real answer for your code, and every blocker it reports is specific and coded. Nothing on this page is silent: everything here is either a compile error or a documented, numbered divergence.
What doesn't compile (yet)
These are rejected at compile time with an SC code, a code frame, and usually a rewrite hint. A non-exhaustive tour of the ones you're most likely to meet:
Language edges
vardeclarations, and loose equality==/!=(they need dynamic coercion semantics; uselet/constand===).- Labeled
break/continue, and jumps crossing afinallyblock (returnthroughfinallyis supported). - Top-level
await— wrap it in anasync function main()and call it. - Generic classes, generic methods, and generic arrows — only top-level generic function declarations monomorphize.
- Generic functions and stdlib methods used as values (
const f = id,const g = Math.floor) — call them directly.
Types and shapes
- Record shapes are exact structs. Passing
{a, b}where{a}is expected is SC2002. Where the compiler does accept a strict field-subset flow, it copies the record — see divergences below. - Union edges: unions used whole where a per-arm answer is needed (reading
u.lengthonstring | string[]— narrow first), union-into-union widening outside the re-tag and width rules, function arms beside data arms. (Printing a whole union is fine:console.log(u)dispatches per arm.) - Watch for tuple inference:
Promise.all([work(1), work(2)])infers a tuple type, and tuple edges (like.joinon a tuple) are fenced. Type the array first:
const jobs: Promise<number>[] = [work(1), work(2), work(3)];
const results = await Promise.all(jobs); // number[] — compilesundefined/nullexist only as union arms, not as standalone values; optional class fields are fenced (optional record fields and optional/default/rest parameters compile).
Standard library
- The type checker sees the full standard library; only the supported surface compiles. Reaching declared-but-unlowered surface is SC2020 with the supported alternatives in the hint — e.g. parts of the regex API (
re.exec),Symbol,globalThis, array/Map/Set methods beyond the lowered sets. - Map keys and Set elements are strings and numbers; other key types are fenced.
The any/unknown boundary
anywithout--dynamicis a compile error (SC2011) — useunknownand a checked cast, or opt into the engine.anyandunknownride locals, parameters, and returns — never class fields, array elements, or union arms (record and tuple fields holdunknown).- Operations on
unknownbeyond the supported surface (truthiness,typeofnarrowing, property access,+,switch,throw) need a checked cast first.
Type surface
scriptc typechecks your program in its own type world: the standard es2025 lib plus its own ambient declarations. In enumerated places those declarations are deliberately different from stock lib or @types/node, typed as what actually compiles — JSON.parse returns unknown (not any), pop() returns T (not T | undefined), the Promise executor's reject reason is pinned to Error. So "programs that typecheck" means scriptc's type world, and a program clean under its own tsc can still be redirected here — but never with a bare type error it can't reproduce: the tightened declarations get a second-chance preflight (a program clean in the project's own type world passes, and the affected sites meet per-site diagnostics with rewrite hints instead), and where the shipped fallback declarations lack a lowering for a stock member (console.table, console.time, ...), the member is declared anyway so the call lands on SC2020 naming the supported alternative. console.log/info/debug/error/warn themselves take unknown arguments and render with Node's console semantics — strings verbatim, everything else through the static util.inspect.
Run scriptc build on a file to get the full, current list for that program — the compiler is always more up to date than this page.
What diverges by design
A static-tier program otherwise produces byte-identical stdout and the same exit code as Node. Every known divergence is deliberate and pinned by the differential test suite; these are the ones with real consequences:
Arrays are dense — invalid indices trap. scriptc arrays have no holes and no undefined elements. Where JS would produce undefined (out-of-bounds read, pop() on empty), the runtime prints a RangeError message and aborts. This catches real bugs — but it means process.argv[2] with no third argument is a trap, not undefined:
// JS habit — traps when argv[2] doesn't exist:
const who = process.argv[2] ?? "world";
// scriptc-safe (the length check is explicit instead of implied):
const who = process.argv.length > 2 ? process.argv[2] : "world";Runtime traps are not catchable. User throw is fully catchable, and runtime failures Node models as exceptions (JSON parse errors, checked-cast failures, fs errors, regex errors) throw real error objects — but the hard-trap set (array index violations and friends) aborts the process rather than throwing.
A lying cast on dynamic data throws instead of corrupting memory — the headline divergence, and the point. JSON.parse(s) as Config with mismatched data throws a catchable error naming the offending path (expected number at $.port, got string) where JS would silently hand you garbage.
Structural width subtyping copies. A record flowing into a strict field-subset shape is copied, not aliased: mutations through the narrower reference are invisible to the original. Same stance at the dynamic boundary: values cross by copy, never by reference.
Object.keys/values/entries and JSON.stringify report a record's declaration order, not per-object insertion order. Identical to Node whenever objects are built in declaration order (the overwhelmingly common case).
Strings are stored as UTF-8. Invisible through .length and the string methods (which compute UTF-16 semantics) — except relational comparison (<, >), which uses code-point order, and surrogate-splitting operations, which produce U+FFFD.
Memory is reference-counted. Acyclic values free deterministically; reference cycles are collected at deterministic collection points, not by a concurrent GC. Cycles that cross the static/island boundary are uncollectable by either side.
Process shape — process.argv[0] is "scriptc" and argv[1] is the binary's path (positions line up with Node; argv[2] onward are your args). The uncaught-exception stderr line reads Uncaught <value> instead of Node's stack-trace block (exit code and pre-throw stdout are identical). Runtime errors carry message and Node's code, but not errno/syscall/path.
Comparator call sequences differ in sort (stable insertion sort here, TimSort in V8 — sorted results are byte-identical for consistent comparators), and localeCompare compares code units, not ICU collation.
Dynamic-tier limits
- The island is quickjs-ng, not V8 — correct, but slower for CPU-bound dependency code. The win is startup, size, memory, deployment shape.
- The island's Node builtins are shims — reimplementations, reported per-builtin in the coverage report, not the real modules.
- Island microtask interleaving: static fibers drain first, then the engine's jobs at loop quiescence — a static
awaitracing a package promise resolves in a documented, deterministic order that can differ from Node's interleaving. --npm-staticand--provenance-sourcesare experimental — see npm Dependencies for the maturity notes.- Cross-compiled
--dynamicbinaries are not supported yet — the engine archive is host-native today.
Tooling gaps
scriptc rundoes not forward extra CLI arguments to the program —buildand invoke the binary directly.- Native FFI is a direct, manifest-declared C ABI link surface. It does not yet support callbacks, variadic calls, structs by value, owned pointer/string/byte returns, runtime dynamic-library loading, or library-mode builds. See Native FFI.
- Numbers are JS-exact f64 everywhere. Integer inference and ownership analysis — the systems-language performance ceiling — are roadmap, not shipped.
- No Windows servers or
child_processyet — see Platform Support.