The question that fixes JavaScript.
WhyJS is a strict superset of JavaScript that compiles to standard ES2022. It fixes JavaScript's most notorious design flaws — nil confusion, loose equality, implicit globals, silent NaN propagation, and more — without breaking ecosystem compatibility. Rename any .js file to .why and it compiles. New safety features are additions, never replacements. The mantra: layer on top, never replace.
npm install -g whyjsFor development:
git clone https://github.com/4shut0sh/whyjs.git
cd whyjs
npm install
npm run build
npm linkRequires Node.js >= 20.
Create hello.why:
val name = 'world'
fn greet(who: string): string {
return 'Hello, ' + who + '!'
}
val msg = greet(name)
console.log(msg)
Compile and run:
why build hello.why
node hello.js
# Hello, world!In 30 seconds you've used val (immutable binding), type annotations, fn declarations, and the WhyJS compiler.
JavaScript has 9 fundamental problems that cause countless bugs and wasted hours. WhyJS fixes them.
| # | JavaScript Problem | WhyJS Fix |
|---|---|---|
| 1 | null vs undefined confusion |
Unified as nil. null, undefined, and nil all compile to undefined. |
| 2 | let vs const vs var ambiguity |
val = immutable (default), let = mutable, const = deep constant. var emits a warning. |
| 3 | == coercion bugs |
== is a parse error. Only === exists. why migrate auto-fixes legacy code. |
| 4 | number is one type |
int, float, decimal are compile-time annotations. In Wasm targets they become hard types. |
| 5 | NaN !== NaN footgun |
NaN is a NumberError in the type system. The checker prevents silent propagation. |
| 6 | [10, 9, 1].sort() → [1, 10, 9] |
Compiler warns on .sort() without comparator on numeric arrays and emits (a, b) => a - b. |
| 7 | parseInt("08") is octal maybe |
parseInt without a radix is a compile-time error. |
| 8 | CommonJS / ESM confusion | ESM-only. require emits a warning; why migrate rewrites to import. |
| 9 | Unhandled promise rejections | A floating async call without await, .catch(), or assignment is a compile-time error (E004). |
fn process(user: User?): string {
return user.name; // ✗ Compile error E042
return user?.name ?? 'Anonymous'; // ✓ Safe
}
Default non-nullable. Use T? for T | nil. Flow-sensitive narrowing with if (x !== nil).
actor Counter {
mut count = 0;
pub async fn increment(): int {
self.count = self.count + 1;
return self.count;
}
}
val counter = new Actor(Counter);
val value = await counter.increment();
Actors run in Web Workers with message-passing RPC. Only pub methods are callable from outside. All messages are validated for serializability. Default 30-second timeout on operations.
@wasm({ memory: 512 })
fn processImage(pixels: int[], width: int, height: int): int[] {
return pixels;
}
val result = await processImage(myPixels, 800, 600);
Functions annotated with @wasm are compiled to run in a sandboxed Worker. Only primitive types (int, float, decimal, bool) and typed arrays are allowed.
import { groupBy, debounce } from '@why/std';
val grouped = groupBy(users, u => u.role);
val safeFn = debounce(expensiveOp, 300);
Only imported functions are inlined into your bundle. Zero dead code.
why migrate ./src # convert JS to WhyJS
why migrate ./src --dry-run # preview changesTransforms: null/undefined → nil, == → ===, var → val/let, this → self, parseInt → add radix, new Date() → Temporal, arguments → rest params, eval → marked, and more.
| Command | Description |
|---|---|
why build <file> |
Compile .why to ES2022 JavaScript |
why migrate <dir> |
Convert .js files to .why semantics |
why test <dir> |
Run Vitest in the given directory |
why dev <file> |
Watch mode — recompile on change |
why fmt <dir> |
Opinionated formatter (2 spaces, single quotes, semicolons) |
why lint <dir> |
Lint and typecheck all .why files |
why bench <file> |
Run benchmarks with statistical output |
why lsp |
Start the Language Server Protocol (stdio) |
why selfhost <dir> |
Analyze TypeScript for WhyJS self-hosting gaps |
why build app.why # writes app.js next to source
why build app.why -o dist/ # custom output directory
why build app.why -t wasm # print @wasm function listing to stderr
why build app.why --stdout # print JS to stdoutTargets: node (default), browser, wasm.
why fmt ./src # format all .why files in place
why fmt ./src --check # exit 1 if any file would change
why fmt ./src --dry-run # alias for --checkOpinionated: 2-space indent, single quotes, semicolons, width 100. Formatted output is re-parsed to verify correctness.
why lint ./src # stylish output (default)
why lint ./src --format json # JSON output
why lint ./src --format compact # one-line-per-diagnosticRuns 10+ lint rules (L001–L010) alongside the type checker. Rules include: no-var, prefer-val, no-eval, no-console, require-radix, unused variables, magic numbers, and more.
why bench bench.why # default: 3 warmup, 30s timeout
why bench bench.why --iterations 1000 # fixed iteration count
why bench bench.why --warmup 5 # warmup runs before measurement
why bench bench.why --timeout 60000 # timeout per benchmark (ms)Outputs statistical summary: mean, median, min, max, standard deviation.
why selfhost ./src # write SELF_HOSTING_GAP.md
why selfhost ./src --output gap.md # custom output pathAnalyzes TypeScript source code and generates a gap report showing which features are needed for the WhyJS compiler to be rewritten in WhyJS itself.
The Language Server Protocol server provides IDE features for .why files:
- Diagnostics — real-time error and warning detection as you type (300ms debounce)
- Hover — type information for any symbol
- Completion — keywords,
@stdfunctions, nil-safety patterns - Code Actions — quick fixes for parseInt radix, nil checks,
==→=== - Go to Definition — jump to top-level declarations
| Keyword | Purpose |
|---|---|
val |
Immutable binding (compiles to const) |
let |
Mutable binding (compiles to let) |
mut |
Mutable state in actors (mut x = 0) |
const |
Deep constant (compiler error on mutable properties) |
var |
Legacy — compiles with a warning |
fn |
Function declaration |
async fn |
Async function declaration |
nil |
Absence of value (compiles to undefined) |
actor |
Concurrent actor declaration |
pub |
Public actor method |
self |
Reference to current instance (compiles to this) |
using |
Deterministic resource cleanup |
test |
Inline test block (compiles to Vitest test()) |
import / export |
ESM modules |
if / else / while |
Control flow |
try / catch / finally |
Error handling |
return |
Return from function |
class / new |
Object-oriented (standard JS semantics) |
await |
Await a promise |
| Decorator | Purpose |
|---|---|
@wasm |
Mark a sync fn for WebAssembly compilation |
@wasm({ memory: N }) |
WebAssembly with memory limit (MB) |
@MainActor |
Guarantee function runs on the main/UI thread |
val name: string = 'Alice'
val age: int = 30
val score: float = 9.5
val precise: decimal = 0.1
val active: bool = true
val missing: nil = nil
val maybe: string? = nil // optional (nullable)
val nums: int[] = [1, 2, 3] // array
val either: int | string = 42 // union
Primitive types: string, int, float, decimal, bool, nil.
Type modifiers: T? (optional), T[] (array), T | U (union).
All type annotations are erased in JS output. In --target wasm they become hard types.
===/!==— strict equality (only option;==is a parse error)??— nil coalescing?.— optional chaining!(postfix) — non-null assertion (emits warning)+,-,*,/,%,**— arithmetic&&,||— logical<,>,<=,>=— comparison
WhyJS provides a built-in actor model for structured concurrency. Actors run in Web Workers with message-passing RPC.
actor Counter {
mut count = 0;
pub async fn increment(): int {
self.count = self.count + 1;
return self.count;
}
pub async fn getCount(): int {
return self.count;
}
async fn reset(): nil {
self.count = 0;
}
}
Actors have mut state fields and async fn methods. Only pub methods are callable from outside; private methods are internal only. The self keyword accesses the actor's state.
val counter = new Actor(Counter);
async fn main(): nil {
val n = await counter.increment();
val c = await counter.getCount();
console.log(c);
}
new Actor(X) creates an actor instance. All method calls return promises and must be awaited.
val task = Actor.spawn(async (signal) => {
return await fetchData(signal);
});
task.cancel();
val results = await Actor.all([task1, task2, task3]);
Functions decorated with @MainActor are guaranteed to run on the main thread:
@MainActor
fn updateUI(msg: string): nil {
console.log(msg);
}
The WhyJS standard library is tree-shaken at compile time — unused functions add zero bytes to output. Import from @std/* or @why/std/*.
| Function | Signature | Description |
|---|---|---|
groupBy |
(arr, key) => Record<string, T[]> |
Group array elements by key function |
keyBy |
(arr, key) => Record<string, T> |
Index array by key function (last wins) |
zip |
(a, b) => [A, B][] |
Pair elements from two arrays |
range |
(start, end, step?) => number[] |
Half-open range [start, end) (max 10M) |
chunk |
(arr, size) => T[][] |
Split array into fixed-size chunks |
shuffle |
(arr) => T[] |
Fisher-Yates shuffle (crypto-seeded) |
| Function | Signature | Description |
|---|---|---|
deepEqual |
(a, b) => boolean |
Deep structural equality (handles cycles) |
deepClone |
(obj) => T |
Deep clone (uses structuredClone when available) |
omit |
(obj, keys) => Omit<T, K> |
Return object without specified keys |
pick |
(obj, keys) => Pick<T, K> |
Return object with only specified keys |
merge |
(target, ...sources) => T |
Shallow merge (blocks __proto__ pollution) |
| Function | Signature | Description |
|---|---|---|
camelCase |
(s) => string |
Convert to camelCase |
kebabCase |
(s) => string |
Convert to kebab-case |
truncate |
(s, len) => string |
Truncate with ellipsis (max 10K) |
slugify |
(s) => string |
URL-safe slug (max 200 chars) |
| Function | Signature | Description |
|---|---|---|
delay |
(ms) => Promise<void> |
Sleep for ms milliseconds (max 30s) |
timeout |
(promise, ms) => Promise<T> |
Reject if promise doesn't resolve in ms |
retry |
(fn, attempts, delayMs?) => Promise<T> |
Retry async function (max 10 attempts) |
debounce |
(fn, ms) => T |
Debounce function calls |
throttle |
(fn, ms) => T |
Throttle function calls |
memoize |
(fn, maxSize?) => T |
Cache results (max 1000 entries default) |
| Function | Signature | Description |
|---|---|---|
clamp |
(value, min, max) => number |
Clamp value to [min, max] range |
inRange |
(value, min, max) => boolean |
Check if value is in range |
random |
(min, max) => number |
Crypto-secure random number |
sumPrecise |
(nums) => number |
Kahan summation for floating-point stability |
mean |
(nums) => number |
Arithmetic mean |
median |
(nums) => number |
Median value |
sortNumeric |
(arr) => number[] |
Numeric ascending sort (returns new array) |
| Function | Signature | Description |
|---|---|---|
isEmail |
(s) => boolean |
Validate email format |
isURL |
(s) => boolean |
Validate HTTP/HTTPS URL |
isUUID |
(s) => boolean |
Validate UUID v1-v5 format |
isEmpty |
(s) => boolean |
True if null, undefined, or empty string |
| Function | Signature | Description |
|---|---|---|
log.info |
(data) => void |
Structured JSON log at info level |
log.warn |
(data) => void |
Structured JSON log at warn level |
log.error |
(data) => void |
Structured JSON log at error level |
| Function | Signature | Description |
|---|---|---|
bench |
(label, fn, iterations?) => BenchResult |
Benchmark a function (max 10M iterations) |
Run benchmarks with why bench file.why to get a formatted results table with mean, median, min, max, and stddev.
| Code | Name | Description |
|---|---|---|
| E001 | Removed equality | == is not allowed; use === |
| E002 | Type mismatch | int and float mixed without explicit conversion |
| E003 | Implicit global | Undeclared variable or assignment to undeclared name |
| E004 | Unhandled promise | Floating async call without await, .catch(), or assignment |
| E005 | parseInt radix | parseInt called without radix argument |
| E006 | Numeric sort | .sort() on numeric array without comparator |
| E042 | Nil access | Member access on nullable type without guard |
| E043 | Nullable to non-null | Passing nullable where non-null type is required |
| E044 | MainActor await | @MainActor function must be awaited from actor code |
| E045 | Unknown std export | Import of nonexistent function from @std/* |
| E046 | Wasm async | @wasm cannot be used with async fn |
| E047 | Wasm untyped | @wasm requires explicit parameter and return types |
| E048 | Wasm DOM | @wasm cannot access document, window, or DOM APIs |
| E049 | Wasm await | await is not allowed inside @wasm functions |
| E050 | Wasm + MainActor | @wasm and @MainActor cannot be combined |
| E100 | MainActor no await | @MainActor call without await |
| E101 | Self outside actor | self used outside actor context |
| E102 | Actor call no await | Actor method call without await |
| E103 | Circular message | Circular message dependency between actors |
| E104 | Non-serializable msg | Message contains non-serializable data |
| E105 | Missing timeout | Actor operation missing timeout |
| E106 | Private access | Access to private (non-pub) actor method |
| E200 | Wasm unsupported param | @wasm parameter has unsupported type |
| E201 | Wasm unsupported return | @wasm return type is unsupported |
| E202 | Wasm unsupported op | Unsupported operation in @wasm function |
| E203 | Wasm non-wasm call | Call to non-@wasm function from @wasm body |
| E204 | Wasm memory limit | @wasm memory exceeds allowed range |
| E205 | Wasm self/this | self/this not allowed in @wasm functions |
| Code | Name | Description |
|---|---|---|
| L001 | Sort comparator | .sort() called without comparator |
| L002 | Unused variable | Declared but never used |
| L003 | Deprecated API | Usage of deprecated function |
| L004 | Magic number | Numeric literal that should be a named constant |
| L005 | Empty catch | catch block with no handling |
| L006 | No var | var usage — use val or let |
| L007 | Prefer val | let that is never reassigned |
| L008 | No eval | eval() usage is a security risk |
| L009 | No console | console.log in production — use @std/log |
| L010 | Require radix | parseInt without radix argument |
src/
ast/nodes.ts — AST node classes (Statement, Expression, TypeAnnotation)
compiler/
lexer.ts — Tokenizer (keywords, operators, nil/null/undefined unification)
parser.ts — Recursive descent parser → AST
typechecker.ts — Type inference, nil safety, diagnostic errors
codegen.ts — AST → ES2022 JavaScript
diagnosticCodes.ts — Stable error/warning codes
prettyPrint.ts — AST → formatted .why source
wasm.ts — @wasm constraint checking and stub generation
wasm-validate.ts — WebAssembly binary validation
hover.ts — Hover type info for LSP
index.ts — compile() / compileAll() / compileForBench()
cli/
index.ts — Commander.js CLI entry
migrate.ts — JS → .why migration engine
lint.ts — Directory-wide lint + typecheck
fmt.ts — Formatter
dev.ts — Watch mode
bench.ts — Benchmark runner
selfhost.ts — Self-hosting gap analysis
runtime/actor.ts — Actor Worker RPC, spawn, all, @MainActor bridge
stdlib/ — Runtime implementations of @std/* functions
std/inlined.ts — Compile-time inlined versions for tree-shaking
lsp/server.ts — Language server (stdio)
errors/DiagnosticError.ts — Rust-style diagnostic formatting
utils/security.ts — Path validation, source size limits, type depth checks
editors/
vscode-whyjs/ — VS Code extension with syntax highlighting and LSP
tests/ — Vitest test suite
e2e/ — End-to-end integration tests
cli/ — CLI command tests
WhyJS enforces security at every level of the compiler pipeline:
- No
evalor dynamic code execution in the compiler itself - Path traversal protection on all file operations
- Source size limits (10MB) to prevent denial-of-service
- Token count limits in the lexer
- Recursion depth limits (100) in the type checker
- Actor message serialization validation (no functions, no cycles)
- Actor operation timeouts (default 30s)
- Prototype pollution protection in
@std/objects.merge - Crypto-secure randomness in
@std/numbers.randomand@std/collections.shuffle - ReDoS-safe regex patterns in
@std/strings - Wasm memory limits (configurable, max 4096MB)
- LSP document size limits (5MB) with debounced validation
- Full JavaScript superset —
for,switch,classbodies, destructuring, spread, template${...}interpolation - Real WebAssembly binary output —
@wasmfunctions emit.wasminstead of asm.js stubs - Generics —
<T>type parameters for type-safe collections and AST - Ohm (PEG) parser — For Why-only syntax extensions layered on TypeScript's grammar
- esbuild bundler integration — Tree-shake
@std/*at the bundle level boundkeyword — Auto-bind class methods to their instanceTemporalbuilt-in — Full Temporal API as a first-class primitive- Self-hosting — Compile the WhyJS compiler in WhyJS
See CONTRIBUTING.md for development setup and guidelines.
MIT — see LICENSE for details.
WhyJS was built in deep collaboration with AI systems. See CREDITS.md for a complete list of contributors.