# What obfuscated code keeps the same (semantic contract)

What AfterPack output preserves of your program's behavior, how each engine release is checked against real code, and the short list of deliberate differences.

Source: https://www.afterpack.dev/docs/semantic-contract

AfterPack changes how your code reads and keeps what it does. An obfuscated build returns the same values, throws the same kinds of errors, writes the same output and makes the same changes to the page as the build you gave it. This page states that contract for engine 0.2.3 and later: what stays the same, how each release is checked, and the few places where the output differs on purpose.

## What stays the same

At every [preset](https://www.afterpack.dev/docs/presets) and with every [seed](https://www.afterpack.dev/docs/config#seed), the obfuscated build keeps:

- **Results and errors.** Functions return the same values. Code that threw still throws an error of the same type.
- **Effects and their order.** Console output, DOM changes, network requests and storage writes happen in the same order, with the same values.
- **Async order.** `async` functions, promises and timers settle in the same order.
- **The global surface.** A classic script defines the same globals as before, so other scripts on the page can still call its functions and read its variables. An ES module adds nothing to `window` and overwrites no global. A file with `import` or `export` is read as a module; for one without, a framework plugin, the CLI or [`sourceType`](https://www.afterpack.dev/docs/config#sourceType) tells the engine.
- **The language rules your code relies on.** Strict-mode errors, `this` and call receivers, getters and setters, class fields, `BigInt` arithmetic, and the scopes of `eval` and `with` behave as written.
- **Names your code reads.** In a file that reads `this.constructor.name` or `new.target.name`, as a custom error class does, classes and constructors keep the names you wrote, with the exceptions under [deliberate differences](#deliberate-differences).

Two builds with different seeds differ in shape, never in behavior.

## What changes by design

- **The text of the code.** What `fn.toString()` returns, line and column numbers, and the names in a stack trace all change. Keep a [source map](https://www.afterpack.dev/docs/config#sourceMap-enabled) private to read production stack traces.
- **Size and speed.** Output is larger, and from `medium` up it does more work per operation. [Performance](https://www.afterpack.dev/docs/performance) has the measured numbers.
- **Code that inspects itself.** When the engine finds code that reads its own structure at runtime, such as `fn.toString()`, it stops the build and names the pattern. Acknowledge the pattern with [`reflection.allow`](https://www.afterpack.dev/docs/config#reflection-allow), or ship the file untouched with [`paths.exclude`](https://www.afterpack.dev/docs/config#paths-exclude).

## How each release is checked

From engine 0.2.3, every engine release passes a semantic gate before it ships. The gate runs each program twice, as written and obfuscated, and compares what each run does: return values, thrown errors, console output, and the order of asynchronous work. A difference fails the release unless it is one of the [deliberate differences](#deliberate-differences) below.

The gate runs:

- **Small targeted programs**, each aimed at one language rule a code transformer can get wrong: classes and private fields, closures and scopes, generators, `async` order, template literals, `BigInt`, `eval` and `with`, strict mode, labels and `switch`. Every preset, many seeds, and both the native engine and the WebAssembly engine.
- **Real libraries** at pinned versions, each driven by a small script: dayjs, Moment.js and Luxon for dates, viem, ethers and @noble/curves for `BigInt`-heavy cryptography, Comlink, lodash's `_.template` and get-intrinsic.
- **Pages in Chromium** that load several classic scripts in order, ES modules with and without imports, inline event handlers and custom elements, and a single-chunk Vue app. Each page must behave the same and leave `window` exactly as the original does, with no new globals and none overwritten.

## Deliberate differences

These and the class names after the table are the only differences the gate allows. Each row involves code that replaces built-ins, wraps instances in a `Proxy`, calls a class without `new` or reads engine error messages, so most applications never meet them. They apply inside classes the engine rewrites, from `light` up; `minify` leaves classes as written.

| Your code | Native behavior | Obfuscated output |
|---|---|---|
| Defines public class fields on an instance that is a `Proxy` (returned from a base constructor) or has one in its prototype chain | Only the proxy's `defineProperty` trap runs | The `has` trap also runs for each field, and for a field the object lacks, the `set` trap and whatever it forwards to |
| Calls a class constructor without `new` | Throws a `TypeError` before any parameter default or destructuring runs | Runs the parameter defaults and destructuring, then throws the same `TypeError` |
| Calls a private method or `super.method()` after replacing `Function.prototype.call`, or on a method with its own `call` property | Calls the method directly | Goes through the replaced `call` |
| Reads the message of a `TypeError` from a private-field check, a second private-field initialization or a class called without `new` | The JavaScript engine's own message | AfterPack's own message, with the same error type |
| Replaces `TypeError`, `Object`, `Object.defineProperty`, `Reflect.construct`, `Reflect.ownKeys`, `WeakMap` or `WeakSet` before the file loads | Classes still use the original built-ins | Rewritten classes use the replacement |

Class and constructor names differ in a few cases, at every preset. A file that reads them only in other ways, such as `err.constructor.name` or `const { name } = new.target`, gets the renamed names. In a file that does read `this.constructor.name` or `new.target.name`, the renamed name still shows for a class constructed as `new (class X {})()`, an anonymous class named by a default parameter, a function constructed from a `switch` case that comes before the case declaring it, and a function constructed only through another reference, such as `new ns.F()`, an alias or `Reflect.construct`.

## What stays as you wrote it

These constructs are left as written wherever rewriting them faithfully is not cheap. The code around them is still renamed and encoded, except around `with` and direct `eval`.

- `async` functions and `await`.
- `delete`, and `typeof` applied to a bare name.
- A function whose `catch (e)` block declares `var e`.
- A `let` or `const` declared directly in a `switch` case.
- An optional call or tag whose callee is in parentheses or is a `super` member: `(o?.m)?.()`, `` (o?.m)`x` ``, `super.m?.()`.
- A class that assigns to its own name, calls a `super` member as an optional call, tag or parenthesized callee, or has a derived constructor without exactly one top-level `super()` call.
- Loops, at `minify` and `light`.
- The scopes around a `with` statement or a direct `eval`. Names in every scope that encloses it, up to the top of the file, keep their original spelling, and globals inside a `with` body are left as written. The build reports it with [`DIAG_DYNAMIC_SCOPE_NATIVE`](https://www.afterpack.dev/docs/diagnostics#complete-reference). Where the evaluated code needs no local variables, call `eval` indirectly, `(0, eval)(code)`, or move the code that needs `eval` or `with` into a file of its own.

## Before engine 0.2.3

Engine 0.2.2 and earlier did not meet this contract in some cases the gate now covers: top-level names of classic scripts shared with other scripts, ES modules without imports, objects such as dates inside template literals, functions used as constructors, class fields over inherited accessors, functions that refer to themselves, `BigInt` arithmetic at `medium` and above, `eval` and `with`, a `catch` block that redeclares its variable with `var`, `async` order, optional calls in parentheses, constant `**` expressions, and class names read at runtime. Rebuild with engine 0.2.3 or later; the [changelog](https://www.afterpack.dev/changelog) lists each fix.

## Report a difference

A difference not listed on this page is a bug. Open an issue with a small reproduction at [github.com/afterpack-dev/afterpack/issues](https://github.com/afterpack-dev/afterpack/issues). Until it is fixed, [`paths.exclude`](https://www.afterpack.dev/docs/config#paths-exclude) ships the affected file untouched.

## Next

- [Diagnostics](https://www.afterpack.dev/docs/diagnostics): every code a build can report, and what to do about it.
- [How AfterPack works](https://www.afterpack.dev/docs/concepts): what the transformations do to your code.
- [Presets](https://www.afterpack.dev/docs/presets): what each level applies.
- [Configuration](https://www.afterpack.dev/docs/config): [`reflection.allow`](https://www.afterpack.dev/docs/config#reflection-allow), [`paths.exclude`](https://www.afterpack.dev/docs/config#paths-exclude) and the other keys this page links.
