PONY λ M2 Modula-2

PHP.CodeCompared.To/JavaScript

An interactive executable cheatsheet comparing PHP and JavaScript

PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM) JavaScript (ES2025)
Output & Running
Hello, World
<?php echo "Hello, World!\n";
console.log("Hello, World!");
JavaScript writes to the console with console.log, which always appends a newline (like PHP's print plus \n) and accepts multiple arguments separated by spaces. There is no <?php tag — a .js file is JavaScript throughout — and statements may end with a semicolon, though they are mostly optional thanks to automatic semicolon insertion.
Running a Script
<?php // php script.php — CLI // Or served by a web server per request. $name = "World"; echo "Hello, {$name}!\n";
// node script.js — server / CLI (Node.js) // Or <script src="script.js"> in a browser page. const name = "World"; console.log(`Hello, ${name}!`);
JavaScript runs in two homes: the browser (its origin) and Node.js on the server — the role PHP traditionally owned. Unlike PHP, where each request starts a fresh process, a Node server is one long-lived process handling many requests on a single event loop, so global state persists between requests and blocking the thread blocks everyone.
Debugging Output
<?php $user = ["name" => "Alice", "roles" => ["admin"]]; print_r($user); var_dump(42, true);
const user = { name: "Alice", roles: ["admin"] }; console.log(user); // structured, expandable console.dir(user, { depth: null }); console.log(42, true);
For inspecting structures, console.log already prints objects and arrays readably — it is the everyday equivalent of print_r, with no true second argument needed to get a string back. console.table renders arrays of objects as a grid, and console.dir with { depth: null } expands deeply nested data that console.log would otherwise truncate.
Variables & Types
Declaring Variables
<?php $name = "Alice"; $age = 30; $age = 31; // reassign freely echo "{$name} is {$age}\n";
const name = "Alice"; // cannot be reassigned let age = 30; age = 31; // let allows reassignment console.log(`${name} is ${age}`);
JavaScript has no $ sigil but it does require a declaration keyword: const for bindings that never change and let for those that do. Prefer const by default and reach for let only when you reassign — a discipline PHP does not enforce. Avoid the old var, which has confusing function-wide scope.
Types & typeof
<?php echo gettype("hello") . "\n"; // string echo gettype(42) . "\n"; // integer echo gettype(3.14) . "\n"; // double echo gettype(true) . "\n"; // boolean echo gettype([1, 2]) . "\n"; // array
console.log(typeof "hello"); // string console.log(typeof 42); // number console.log(typeof 3.14); // number ← no separate int/float console.log(typeof true); // boolean console.log(typeof [1, 2]); // object ← arrays are objects console.log(Array.isArray([1, 2])); // true
JavaScript's typeof is coarser than gettype. There is a single number type — no integer/double split — so 42 and 3.14 report the same. And typeof [1,2] is "object", since arrays are a kind of object; use Array.isArray() to test for an array specifically.
null vs undefined
<?php // PHP has ONE "absence" value: null. $value = null; var_dump(isset($value)); // false (null counts as unset) $other; // declared-but-unset is also null-ish var_dump($missing ?? "default");
// JavaScript has TWO absence values. let value = null; // explicitly "no value" let other; // undefined — declared but never assigned console.log(value); // null console.log(other); // undefined console.log(typeof undefined); // "undefined" console.log(value ?? "default"); // default (?? covers both)
A real adjustment: JavaScript splits PHP's single null into two. undefined means "never assigned" (a missing property or unset variable), while null means "deliberately empty". They are distinct (null !== undefined), but the nullish coalescing operator ?? treats both as absent, so x ?? fallback behaves like PHP's ?? against either.
Concatenation vs Addition
<?php // PHP keeps them separate: . concatenates, + always adds. echo "1" . 2 . "\n"; // "12" echo "1" + 2 . "\n"; // 3 (numeric strings add) echo 5 . 3 . "\n"; // "53" (numbers concatenate)
// JavaScript overloads + for BOTH, and string wins. console.log("1" + 2); // "12" — concatenation console.log(1 + 2); // 3 — addition console.log(5 + "3"); // "53" — number coerced to string console.log("5" - 1); // 4 — but - forces numeric!
PHP wisely uses . for concatenation and + only for math, so it never guesses. JavaScript overloads +: if either side is a string, it concatenates; otherwise it adds. This is the source of countless bugs — 5 + "3" is "53" but "5" - 1 is 4, because - has no string meaning and forces numeric conversion.
Constants
<?php const MAX_USERS = 100; define("APP_NAME", "Demo"); echo MAX_USERS . "\n"; echo APP_NAME . "\n";
const MAX_USERS = 100; const APP_NAME = "Demo"; console.log(MAX_USERS); console.log(APP_NAME); // const only blocks reassignment, not mutation: const roles = ["admin"]; roles.push("user"); // allowed — the array is still mutable console.log(roles);
const is JavaScript's constant, but with one caveat PHP's const shares: it freezes the binding, not the value. A const array or object can still be mutated in place; only reassigning the variable itself is forbidden. To make the contents immutable too, wrap them in Object.freeze().
Strings
String Interpolation
<?php $user = "Alice"; $age = 30; echo "Name: $user\n"; echo "Next year: " . ($age + 1) . "\n"; echo "Name: {$user}, age {$age}\n";
const user = "Alice"; const age = 30; console.log(`Name: ${user}`); console.log(`Next year: ${age + 1}`); console.log(`Name: ${user}, age ${age}`);
JavaScript interpolates only inside template literals — strings written with backticks (`...`), not the usual quotes. The placeholder is ${ ... } and, like PHP's {$...}, it accepts any expression. Ordinary "..." and '...' strings do not interpolate at all, so a stray "$user" stays literal.
String Operations
<?php $text = "Hello, World"; echo strlen($text) . "\n"; // 12 echo strtoupper($text) . "\n"; // HELLO, WORLD echo str_replace("World", "JS", $text) . "\n"; echo substr($text, 0, 5) . "\n"; // Hello echo strpos($text, "World") . "\n"; // 7
const text = "Hello, World"; console.log(text.length); // 12 (property, not call) console.log(text.toUpperCase()); // HELLO, WORLD console.log(text.replaceAll("World", "JS")); console.log(text.slice(0, 5)); // Hello console.log(text.indexOf("World")); // 7
As in many modern languages, PHP's global string functions become methods on the string. Note length is a property (no parentheses), while the rest are methods. replace swaps the first match and replaceAll swaps every one — unlike PHP's str_replace, which always replaces all.
Split & Join
<?php $csv = "apple,banana,cherry"; $fruits = explode(",", $csv); print_r($fruits); echo implode(" | ", $fruits) . "\n";
const csv = "apple,banana,cherry"; const fruits = csv.split(","); console.log(fruits); console.log(fruits.join(" | "));
JavaScript's split and join mirror explode and implode, but the receiver flips to match where the data lives: you split a string and join an array. split takes the separator (a string or a regular expression) as its argument, and join defaults to a comma if you pass nothing.
Multi-line Strings
<?php $name = "Alice"; $message = <<<TEXT Dear {$name}, Welcome aboard. Regards TEXT; echo $message . "\n";
const name = "Alice"; const message = `Dear ${name}, Welcome aboard. Regards`; console.log(message);
A template literal spans multiple lines as-is — no heredoc syntax needed. Every line break and space between the backticks is preserved literally, so unlike PHP's heredoc you control indentation by where you place the text, and there is no closing-label rule. Template literals also power "tagged templates", a metaprogramming hook PHP has no analogue for.
Arrays & Objects
Lists: Arrays
<?php $fruits = ["apple", "banana", "cherry"]; echo $fruits[0] . "\n"; echo count($fruits) . "\n"; $fruits[] = "date"; // append print_r($fruits);
const fruits = ["apple", "banana", "cherry"]; console.log(fruits[0]); console.log(fruits.length); fruits.push("date"); // append console.log(fruits);
A plain JavaScript array is the integer-indexed half of PHP's array. You append with push rather than [], read the size from the length property, and — crucially — keys are only ever sequential integers. The string-keyed half of a PHP array becomes a separate object or Map, shown next.
Maps: Objects & Map
<?php $ages = [ "Alice" => 30, "Bob" => 25, ]; echo $ages["Alice"] . "\n"; $ages["Carol"] = 35; foreach ($ages as $name => $age) { echo "{$name}: {$age}\n"; }
const ages = { Alice: 30, Bob: 25, }; console.log(ages.Alice); // dot access console.log(ages["Bob"]); // or bracket ages.Carol = 35; for (const [name, age] of Object.entries(ages)) { console.log(`${name}: ${age}`); }
The string-keyed PHP array maps to a plain object literal { }, whose keys you read with dot or bracket notation. Iterating needs Object.entries() to get key/value pairs. For a true dictionary — any key type, guaranteed order, a real .size, and safe iteration — use a Map (new Map([["Alice", 30]])), which avoids the prototype pitfalls of using objects as hash tables.
Map, Filter, Reduce
<?php $numbers = [1, 2, 3, 4, 5]; $doubled = array_map(fn($n) => $n * 2, $numbers); $evens = array_filter($numbers, fn($n) => $n % 2 === 0); $total = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0); print_r($doubled); print_r(array_values($evens)); echo $total . "\n";
const numbers = [1, 2, 3, 4, 5]; const doubled = numbers.map(n => n * 2); const evens = numbers.filter(n => n % 2 === 0); const total = numbers.reduce((carry, n) => carry + n, 0); console.log(doubled); console.log(evens); console.log(total);
These are methods on the array, so they chain and always put the collection first — no more recalling whether the callback or the array is the first argument as in array_map vs array_filter. And filter returns a clean re-indexed array, sparing you PHP's array_values() dance after array_filter leaves gaps in the keys.
Destructuring
<?php $point = [3, 4]; [$x, $y] = $point; echo "{$x}, {$y}\n"; $user = ["name" => "Alice", "role" => "admin"]; ["name" => $name, "role" => $role] = $user; echo "{$name} ({$role})\n";
const point = [3, 4]; const [x, y] = point; console.log(`${x}, ${y}`); const user = { name: "Alice", role: "admin" }; const { name, role } = user; // by key name console.log(`${name} (${role})`);
JavaScript destructures both arrays (positionally, with [ ]) and objects (by key, with { }) — the latter is more concise than PHP's ["name" => $name] form because matching variable names are implied. Defaults (const { role = "guest" } = user) and renaming ({ name: fullName }) make it a workhorse for unpacking function arguments and API responses.
Spread & Rest
<?php $a = [1, 2]; $b = [3, 4]; $combined = [...$a, ...$b]; print_r($combined); $base = ["host" => "local"]; $config = [...$base, "port" => 5432]; print_r($config);
const a = [1, 2]; const b = [3, 4]; const combined = [...a, ...b]; console.log(combined); const base = { host: "local" }; const config = { ...base, port: 5432 }; console.log(config);
JavaScript's spread operator is the same ... as PHP's, and it works for both arrays and objects — { ...base, port: 5432 } is the idiomatic way to clone-and-extend an object, with later keys winning. It is the standard tool for non-mutating updates, which matters because JavaScript objects are reference types (see the Gotchas section).
Membership & Keys
<?php $fruits = ["apple", "banana"]; var_dump(in_array("apple", $fruits)); // true $ages = ["Alice" => 30]; var_dump(array_key_exists("Alice", $ages)); // true var_dump(isset($ages["Bob"])); // false
const fruits = ["apple", "banana"]; console.log(fruits.includes("apple")); // true const ages = { Alice: 30 }; console.log("Alice" in ages); // true console.log(ages.Bob === undefined); // true console.log(Object.keys(ages)); // ["Alice"]
For arrays, includes replaces in_array. For objects, the in operator replaces array_key_exists, and Object.keys() / Object.values() / Object.entries() give you the keys, values, and pairs. Reading a missing object property returns undefined rather than emitting a warning as PHP does for a missing array key.
Control Flow
Conditionals
<?php $age = 20; if ($age >= 65) { echo "senior\n"; } elseif ($age >= 18) { echo "adult\n"; } else { echo "minor\n"; }
const age = 20; if (age >= 65) { console.log("senior"); } else if (age >= 18) { console.log("adult"); } else { console.log("minor"); }
Conditionals are nearly identical — both keep the parentheses and braces. The one syntactic difference is the chained keyword: JavaScript writes else if as two words, where PHP fuses them into elseif. (PHP actually accepts else if too, but elseif is its convention.)
switch vs match
<?php $status = 2; $label = match ($status) { 1 => "active", 2, 3 => "pending", default => "unknown", }; echo $label . "\n";
const status = 2; let label; switch (status) { case 1: label = "active"; break; case 2: case 3: label = "pending"; break; default: label = "unknown"; } console.log(label);
PHP 8's match is genuinely nicer than JavaScript's switch: it is an expression that returns a value, has no fall-through, and uses strict === comparison. JavaScript only has the older switch statement — it falls through unless you write break, does not produce a value, and groups multiple values by stacking empty case labels. Many JS developers reach for an object lookup or if/else if instead.
Loops
<?php for ($i = 0; $i < 3; $i++) { echo $i . "\n"; } foreach (["a", "b", "c"] as $letter) { echo $letter . "\n"; }
for (let i = 0; i < 3; i++) { console.log(i); } for (const letter of ["a", "b", "c"]) { console.log(letter); }
The C-style for is the same. PHP's foreach ($array as $value) becomes for (const value of array) — note of, which iterates values. Beware its sibling for...in, which iterates keys (and inherited ones) and is almost never what you want for an array; use for...of or array.forEach().
Truthiness
<?php // PHP falsy: 0, 0.0, "", "0", [], null, false. foreach ([0, "", "0", [], null, "hi"] as $v) { echo ($v ? "truthy" : "falsy") . "\n"; }
// JS falsy: 0, 0n, "", null, undefined, NaN, false. for (const v of [0, "", "0", [], null, "hi"]) { console.log(v ? "truthy" : "falsy"); } // "0" is TRUTHY in JS, [] is TRUTHY in JS.
The falsy sets overlap but differ in two traps. In JavaScript, "0" is truthy (a non-empty string), whereas PHP treats it as falsy — a classic bug when checking form input. And an empty array [] is truthy in JavaScript (objects are always truthy), while it is falsy in PHP. JavaScript adds undefined and NaN to the falsy list.
Functions
Defining Functions
<?php function greet(string $name): string { return "Hello, {$name}"; } echo greet("Alice") . "\n";
function greet(name) { return `Hello, ${name}`; } console.log(greet("Alice"));
The function keyword and explicit return are the same; JavaScript simply drops the type annotations (those return when you adopt TypeScript). A function declaration like this is "hoisted" — usable before its definition appears in the file — unlike a function assigned to a const, which is not.
Default Arguments
<?php function connect(string $host, int $port = 5432): string { return "{$host}:{$port}"; } echo connect("db.local") . "\n"; echo connect("db.local", 6432) . "\n";
function connect(host, port = 5432) { return `${host}:${port}`; } console.log(connect("db.local")); console.log(connect("db.local", 6432));
Default parameter values look identical to PHP's. One bonus: a JavaScript default can be any expression, including one that references earlier parameters (function f(a, b = a * 2)) — PHP restricts defaults to constant expressions. A default applies whenever the argument is omitted or explicitly undefined.
Named Arguments
<?php function makeUser(string $name, bool $admin = false, bool $active = true): string { return "{$name} admin=" . ($admin ? "y" : "n"); } // PHP 8 named arguments — skip by name: echo makeUser("Alice", active: false, admin: true) . "\n";
function makeUser({ name, admin = false, active = true }) { return `${name} admin=${admin ? "y" : "n"}`; } // JS idiom: pass a single "options object". console.log(makeUser({ name: "Alice", active: false, admin: true }));
JavaScript has no named-argument syntax like PHP 8's admin: true. The established idiom is to accept a single options object and destructure it in the parameter list, which gives the same order-independent, self-documenting call site. Defaults go right in the destructuring pattern, as shown.
Rest Parameters
<?php function sum(int ...$numbers): int { return array_sum($numbers); } echo sum(1, 2, 3, 4) . "\n"; // 10 $values = [5, 6, 7]; echo sum(...$values) . "\n"; // 18
function sum(...numbers) { return numbers.reduce((total, n) => total + n, 0); } console.log(sum(1, 2, 3, 4)); // 10 const values = [5, 6, 7]; console.log(sum(...values)); // 18
JavaScript's rest parameter uses the same ... token PHP adopted, collecting extra arguments into a real array (so you immediately get map, reduce, and the rest). Spreading an array at the call site with sum(...values) works identically. There is no array_sum; reduce is the general-purpose fold.
Arrow Functions
<?php $square = fn($n) => $n * $n; echo $square(5) . "\n"; $add = function ($a, $b) { return $a + $b; }; echo $add(2, 3) . "\n";
const square = n => n * n; console.log(square(5)); const add = (a, b) => { return a + b; }; console.log(add(2, 3));
JavaScript's arrow function n => n * n is the direct counterpart of PHP's fn($n) => .... A single-expression body returns implicitly; a braced body needs an explicit return. Beyond brevity, arrow functions differ from regular functions in one important way — they do not bind their own this — which the Closures section covers.
Closures
Variable Capture
<?php $factor = 3; // PHP closures need an explicit "use" clause: $scale = function ($n) use ($factor) { return $n * $factor; }; echo $scale(10) . "\n"; // 30
let factor = 3; // JS closures capture surrounding variables automatically: const scale = n => n * factor; console.log(scale(10)); // 30 factor = 5; console.log(scale(10)); // 50 — sees the live variable
JavaScript closures capture lexical variables automatically — there is no use ($factor) clause. And because they close over the live binding (not a copy), changing factor afterward is visible inside the closure. This is the same behavior as PHP's use (&$factor) by-reference form, but it is the default rather than opt-in.
First-Class Functions
<?php function applyTwice(callable $fn, $value) { return $fn($fn($value)); } $increment = fn($n) => $n + 1; echo applyTwice($increment, 5) . "\n"; // 7 // PHP 8.1 first-class callable: $upper = array_map(strtoupper(...), ["a", "b"]); print_r($upper);
function applyTwice(fn, value) { return fn(fn(value)); } const increment = n => n + 1; console.log(applyTwice(increment, 5)); // 7 // Any function is already a value — just name it: const upper = ["a", "b"].map(s => s.toUpperCase()); console.log(upper);
In JavaScript functions are values from the start — there is no callable type hint and no (...) first-class-callable syntax, because a function name already is a reference to the function. You pass increment, store it, and return it like any other value, which is why callbacks feel so lightweight.
Arrow Functions & this
<?php class Counter { private array $items = [10, 20]; private int $base = 100; public function totals(): array { // $this is captured automatically inside the closure: return array_map(fn($n) => $n + $this->base, $this->items); } } print_r((new Counter())->totals());
class Counter { items = [10, 20]; base = 100; totals() { // Arrow fn keeps the method's 'this'; a regular fn would not. return this.items.map(n => n + this.base); } } console.log(new Counter().totals());
Here is the practical reason arrow functions exist. A regular JavaScript function gets its own this determined by how it is called, so inside a map callback a plain function would lose the object and this.base would break. An arrow function has no this of its own and inherits the enclosing method's — giving you the predictable behavior PHP's $this always has.
Classes & OOP
Defining a Class
<?php class Greeter { public function __construct(private string $name) {} public function greet(): string { return "Hi, {$this->name}"; } } $greeter = new Greeter("Alice"); echo $greeter->greet() . "\n";
class Greeter { constructor(name) { this.name = name; } greet() { return `Hi, ${this.name}`; } } const greeter = new Greeter("Alice"); console.log(greeter.greet());
JavaScript's class will feel familiar: the constructor is named constructor, members are reached through this. (a dot, not ->), and you instantiate with new. There is no constructor property promotion, so you assign this.name = name by hand. Under the hood this is syntax sugar over prototypes, but day to day it reads like PHP OOP.
Properties & Privacy
<?php class Account { public string $owner = "unknown"; private int $balance = 0; public function deposit(int $amount): void { $this->balance += $amount; } public function getBalance(): int { return $this->balance; } } $account = new Account(); $account->deposit(100); echo $account->getBalance() . "\n";
class Account { owner = "unknown"; // public field #balance = 0; // # marks it truly private deposit(amount) { this.#balance += amount; } getBalance() { return this.#balance; } } const account = new Account(); account.deposit(100); console.log(account.getBalance());
Class fields are declared directly in the body, like PHP's typed properties but without a visibility keyword — fields are public by default. For real privacy, JavaScript uses a # prefix (#balance): unlike PHP's private, which is enforced softly, a # field is genuinely inaccessible from outside the class, even via bracket notation.
Inheritance
<?php class Animal { public function __construct(protected string $name) {} public function speak(): string { return "..."; } } class Dog extends Animal { public function speak(): string { return "{$this->name} says Woof"; } } echo (new Dog("Rex"))->speak() . "\n";
class Animal { constructor(name) { this.name = name; } speak() { return "..."; } } class Dog extends Animal { speak() { return `${this.name} says Woof`; } } console.log(new Dog("Rex").speak());
Inheritance uses the same extends keyword. To call a parent constructor or method you write super(...) / super.speak() rather than PHP's parent::. One rule to remember: a subclass constructor must call super() before it touches this, because the parent is responsible for initializing the instance.
Static Members
<?php class MathUtil { const PI = 3.14159; public static function square(int $n): int { return $n * $n; } } echo MathUtil::square(5) . "\n"; echo MathUtil::PI . "\n";
class MathUtil { static PI = 3.14159; static square(n) { return n * n; } } console.log(MathUtil.square(5)); console.log(MathUtil.PI);
JavaScript marks both static methods and static fields with the static keyword, and you access them through the class name with a dot — MathUtil.square(5) — not PHP's :: scope-resolution operator. JavaScript has no separate const for class constants; a static field fills that role.
Getters & Setters
<?php class Temperature { public function __construct(private float $celsius) {} // PHP uses explicit accessor methods or __get magic. public function getFahrenheit(): float { return $this->celsius * 9 / 5 + 32; } } $temp = new Temperature(25); echo $temp->getFahrenheit() . "\n"; // 77
class Temperature { constructor(celsius) { this.celsius = celsius; } // get makes it look like a property, not a method call: get fahrenheit() { return this.celsius * 9 / 5 + 32; } } const temp = new Temperature(25); console.log(temp.fahrenheit); // 77 — no parentheses
JavaScript has built-in get and set accessors: a method prefixed with get is invoked by reading temp.fahrenheit as if it were a plain field — no parentheses. This is cleaner than PHP's convention of getFahrenheit() methods or its __get magic method, and the matching set fahrenheit(value) intercepts assignments.
Modules
Modules: import / export
<?php // math.php namespace Math; function square(int $n): int { return $n * $n; } // main.php // require 'math.php'; // echo \Math\square(5);
// math.js export function square(n) { return n * n; } export const PI = 3.14159; // main.js import { square, PI } from "./math.js"; console.log(square(5), PI);
JavaScript modules are explicit about what crosses file boundaries: you export specific names and import exactly what you need, rather than PHP's require that pulls in a whole file and relies on namespaces to avoid collisions. Each module has its own scope — nothing is global by accident. This example cannot run in the single-file sandbox here, but it is the backbone of every real project.
Default Exports & Packages
<?php // Composer autoloading maps namespaces to files: // composer require monolog/monolog // use Monolog\Logger; // $log = new Logger("app"); echo "PHP uses Composer + PSR-4 autoload\n";
// A module may have ONE default export: // export default class Logger { /* ... */ } // import Logger from "./logger.js"; // any name you like // Third-party packages come from npm: // npm install lodash // import groupBy from "lodash/groupBy"; console.log("JS uses npm + ES modules");
Alongside named exports, a module can have a single default export that the importer may name anything — common for "the one main thing" a file provides. Dependencies are installed from npm (the registry analogous to Packagist) into a local node_modules folder, with package.json playing the role of composer.json.
Error Handling
try / catch / finally
<?php function parsePort(string $text): int { if (!ctype_digit($text)) { throw new InvalidArgumentException("bad port: {$text}"); } return (int) $text; } try { echo parsePort("abc") . "\n"; } catch (InvalidArgumentException $error) { echo "Error: " . $error->getMessage() . "\n"; } finally { echo "done\n"; }
function parsePort(text) { if (!/^\d+$/.test(text)) { throw new Error(`bad port: ${text}`); } return Number(text); } try { console.log(parsePort("abc")); } catch (error) { console.log(`Error: ${error.message}`); } finally { console.log("done"); }
The structure is the same — try / catch / finally — but JavaScript's catch does not filter by exception type the way PHP's catch (InvalidArgumentException $e) does. You catch every error in one block and inspect it (error instanceof TypeError, error.message) to decide what to do. The message is on .message, not ->getMessage().
Custom Errors
<?php class PaymentError extends \RuntimeException {} try { throw new PaymentError("card declined"); } catch (PaymentError $error) { echo "Caught: {$error->getMessage()}\n"; }
class PaymentError extends Error { constructor(message) { super(message); this.name = "PaymentError"; } } try { throw new PaymentError("card declined"); } catch (error) { if (error instanceof PaymentError) { console.log(`Caught: ${error.message}`); } else { throw error; // re-throw what we don't handle } }
Custom errors subclass the built-in Error. Two conventions matter: call super(message) so the message and stack trace are set up, and assign this.name so the error prints with the right label. Because catch is untyped, you select your error with instanceof and re-throw anything you did not mean to handle.
You Can Throw Anything
<?php // PHP only lets you throw Throwable instances. try { throw new Exception("must be an exception object"); } catch (Throwable $error) { echo $error->getMessage() . "\n"; }
// JS lets you throw ANY value — but you shouldn't. try { throw "a bare string"; // legal, but loses stack trace } catch (error) { console.log(typeof error, error); // string a bare string } // Always throw an Error so .message and .stack exist.
Unlike PHP, which only permits throwing objects implementing Throwable, JavaScript lets you throw any value — a string, a number, an object. This is a trap: a thrown non-Error has no .message or .stack, so defensive code must handle the possibility. The rule of thumb is simple — always throw an Error (or a subclass).
Async & Promises
Blocking vs the Event Loop
<?php // PHP is synchronous: each line finishes before the next starts. echo "first\n"; $result = strtoupper("second"); // runs immediately, blocks echo $result . "\n"; echo "third\n";
// JS runs on an event loop; some work is deferred. console.log("first"); setTimeout(() => console.log("deferred"), 0); console.log("third"); // Output order: first, third, deferred // — the timer callback runs after the current code finishes.
This is the deepest model difference. PHP executes top to bottom and blocks on I/O — a database call pauses the whole script until it returns. JavaScript runs on a single-threaded event loop: I/O is non-blocking, and callbacks (timers, network responses) are queued to run after the current synchronous code completes. Understanding this ordering is essential to everything that follows.
Promises
<?php // PHP: fetching is a normal blocking call that returns a value. function fetchUser(int $id): array { // Imagine a DB/HTTP call here — it blocks until done. return ["id" => $id, "name" => "Alice"]; } $user = fetchUser(1); echo $user["name"] . "\n";
// JS: async work returns a Promise — a value that arrives later. function fetchUser(id) { return new Promise(resolve => { setTimeout(() => resolve({ id, name: "Alice" }), 10); }); } fetchUser(1).then(user => console.log(user.name));
Where a PHP function blocks and hands back the finished value, an asynchronous JavaScript function hands back a Promise immediately — a placeholder for a value that will exist later. You register what to do on completion with .then(callback) (and failures with .catch). The script does not wait; it moves on and runs your callback when the result is ready.
async / await
<?php function fetchUser(int $id): array { return ["id" => $id, "name" => "Alice"]; } function greet(int $id): string { $user = fetchUser($id); // blocks, returns the value return "Hello, {$user['name']}"; } echo greet(1) . "\n";
function fetchUser(id) { return Promise.resolve({ id, name: "Alice" }); } async function greet(id) { const user = await fetchUser(id); // pause until the Promise settles return `Hello, ${user.name}`; } (async () => { console.log(await greet(1)); })();
The async/await syntax lets asynchronous code read like the synchronous PHP beside it: await a Promise and you get its resolved value on the next line, as if it had blocked. It did not — the function yields control to the event loop while waiting. An async function always returns a Promise, and await is only valid inside one (hence the wrapping IIFE here).
Concurrent Work
<?php // PHP: requests run one after another, total time = sum. function fetchPrice(string $item): int { return strlen($item) * 10; // pretend this is slow I/O } $apple = fetchPrice("apple"); $pear = fetchPrice("pear"); echo ($apple + $pear) . "\n";
function fetchPrice(item) { return Promise.resolve(item.length * 10); } (async () => { // Promise.all runs them concurrently — total time = the slowest. const [apple, pear] = await Promise.all([ fetchPrice("apple"), fetchPrice("pear"), ]); console.log(apple + pear); })();
This is where the event loop pays off. In synchronous PHP, two slow calls run back to back and you wait for both in sequence. In JavaScript, kick off both Promises and await Promise.all([...]) — they progress concurrently on the single thread (the I/O overlaps), so the total wait is the slowest one, not the sum. Promise.allSettled is the variant that does not reject on the first failure.
Enums
Enums
<?php enum Status: string { case Active = "active"; case Pending = "pending"; } $status = Status::Pending; echo $status->value . "\n"; // pending echo Status::from("active")->name . "\n"; // Active
// JS has no enum. The idiom is a frozen object of constants. const Status = Object.freeze({ Active: "active", Pending: "pending", }); const status = Status.Pending; console.log(status); // pending console.log(Object.values(Status)); // ["active","pending"]
Here PHP is clearly ahead: its first-class enum (8.1) has cases, backing values, and helpers like ::from(). JavaScript has no enum type at all. The common substitute is an object of constants wrapped in Object.freeze() to prevent accidental modification — adequate, but without the type safety, exhaustiveness, or methods PHP enums provide. (TypeScript adds a real enum.)
Behavior on Enums
<?php enum Suit: string { case Hearts = "H"; case Spades = "S"; public function color(): string { return match ($this) { Suit::Hearts => "red", Suit::Spades => "black", }; } } echo Suit::Hearts->color() . "\n";
const Suit = Object.freeze({ Hearts: "H", Spades: "S" }); const COLORS = Object.freeze({ H: "red", S: "black" }); function colorOf(suit) { return COLORS[suit]; } console.log(colorOf(Suit.Hearts)); // red
PHP enums can carry methods directly on each case — behavior and value travel together. JavaScript's frozen-object enums are plain data, so associated behavior lives elsewhere: a companion lookup object (as here) or a standalone function. When the behavior grows substantial, a full class with static instances is the closer structural match.
Standard Library
JSON
<?php $payload = ["name" => "Alice", "roles" => ["admin", "user"]]; $json = json_encode($payload); echo $json . "\n"; $parsed = json_decode($json, true); echo $parsed["name"] . "\n";
const payload = { name: "Alice", roles: ["admin", "user"] }; const json = JSON.stringify(payload); console.log(json); const parsed = JSON.parse(json); console.log(parsed.name);
JSON is native to JavaScript — it is JavaScript object syntax — so JSON.stringify and JSON.parse need no array-vs-object flag like json_decode($json, true): objects parse to objects and arrays to arrays, exactly as written. JSON.stringify(value, null, 2) pretty-prints with two-space indentation, the equivalent of JSON_PRETTY_PRINT.
Nullish & Optional Chaining
<?php $config = ["timeout" => 30]; $timeout = $config["timeout"] ?? 60; echo $timeout . "\n"; // 30 $user = ["profile" => null]; $city = $user["profile"]["city"] ?? "unknown"; // @-suppressed in <8 echo $city . "\n";
const config = { timeout: 30 }; const timeout = config.timeout ?? 60; console.log(timeout); // 30 const user = { profile: null }; const city = user.profile?.city ?? "unknown"; console.log(city); // unknown
Both operators carried over almost verbatim. ?? supplies a fallback only when the left side is null or undefined (not for 0 or ""), and the optional chaining operator ?. is JavaScript's ?->: it short-circuits to undefined the moment a link in the chain is nullish, instead of throwing. They pair naturally, as shown.
Sorting
<?php $words = ["banana", "apple", "cherry"]; sort($words); // mutates, alphabetical print_r($words); $numbers = [10, 2, 33, 4]; sort($numbers); // numeric, as expected print_r($numbers);
const words = ["banana", "apple", "cherry"]; words.sort(); // mutates, alphabetical console.log(words); const numbers = [10, 2, 33, 4]; numbers.sort((a, b) => a - b); // MUST pass a comparator! console.log(numbers); // [2, 4, 10, 33]
A genuine trap: JavaScript's default sort converts elements to strings, so [10, 2, 33, 4] sorts to [10, 2, 33, 4] lexicographically — "10" before "2". For numbers you must supply a comparator, (a, b) => a - b. Like PHP's sort, it mutates the array in place and returns it (there is no separate non-mutating sort until toSorted).
Numbers & Formatting
<?php echo intval("42px") . "\n"; // 42 echo round(3.14159, 2) . "\n"; // 3.14 echo number_format(1234567.5, 2) . "\n"; // 1,234,567.50 echo (3 <=> 5) . "\n"; // -1 (spaceship)
console.log(parseInt("42px", 10)); // 42 console.log(Number("42px")); // NaN (strict) console.log((3.14159).toFixed(2)); // "3.14" console.log((1234567.5).toLocaleString("en-US")); // 1,234,567.5 console.log(Math.sign(3 - 5)); // -1
JavaScript has two string-to-number paths: parseInt/parseFloat read a leading number and ignore trailing junk (like PHP's intval), while Number() is strict and yields NaN for "42px". There is no number_format; toLocaleString handles grouping and locale, and toFixed rounds to a fixed number of decimals (returning a string). JavaScript has no spaceship operator.
⚠ Gotchas for PHP Devs
Always Use ===
<?php // PHP 8 tightened == but it still coerces. var_dump("1" == 1); // true — loose var_dump("1" === 1); // false — strict var_dump(0 == ""); // false in PHP 8 (was true in 7) var_dump(null == false);// true
// JS == has its OWN coercion rules — different from PHP's. console.log("1" == 1); // true console.log("1" === 1); // false — strict console.log(0 == ""); // true — differs from PHP 8! console.log(null == undefined); // true console.log(null == false); // false — differs from PHP!
Both languages have a loose == and a strict ===, but their coercion tables do not match0 == "" is false in PHP 8 yet true in JavaScript, and null == false flips the other way. Do not assume your PHP intuition transfers. The universal advice is the same in both: always use === and never reason about == at all.
Objects Are References
<?php // PHP arrays COPY on assignment (value semantics). $original = ["count" => 1]; $copy = $original; $copy["count"] = 99; echo $original["count"] . "\n"; // 1 — unchanged
// JS objects and arrays are SHARED on assignment. const original = { count: 1 }; const copy = original; copy.count = 99; console.log(original.count); // 99 — changed too! const real = { ...original }; // spread makes a shallow copy real.count = 5; console.log(original.count); // still 99
A major trap coming from PHP, whose arrays copy on assignment. JavaScript objects and arrays are reference types: copy = original makes both names point at the same object, so mutating one mutates both. To copy, spread into a new literal ({ ...original } or [...list]) for a shallow clone, or structuredClone(original) for a deep one. Primitives (numbers, strings, booleans) still copy by value.
Numbers Are All Floats
<?php echo 7 / 2 . "\n"; // 3.5 echo intdiv(7, 2) . "\n"; // 3 echo (0.1 + 0.2) . "\n"; // 0.3 (PHP rounds for display) var_dump(0.1 + 0.2 == 0.3); // false (still float!)
console.log(7 / 2); // 3.5 console.log(Math.trunc(7 / 2)); // 3 — no integer division operator console.log(0.1 + 0.2); // 0.30000000000000004 console.log(0.1 + 0.2 === 0.3); // false console.log(Number.isInteger(4.0)); // true — 4.0 IS 4
JavaScript has a single IEEE-754 double number type, so 7 / 2 is 3.5 (use Math.trunc or Math.floor for integer division — there is no intdiv). The float-rounding surprise 0.1 + 0.2 !== 0.3 exists in PHP too, but JavaScript shows the full 0.30000000000000004 rather than hiding it on display. For exact large integers, JavaScript adds a separate BigInt type (10n).
this Is Not Always the Object
<?php // In PHP, $this inside a method ALWAYS refers to the object. class Counter { private int $count = 5; public function show(): void { echo $this->count . "\n"; // always works } } (new Counter())->show();
class Counter { count = 5; show() { console.log(this.count); } } const counter = new Counter(); counter.show(); // 5 — called as a method const loose = counter.show; // loose(); // TypeError: 'this' is undefined! const bound = counter.show.bind(counter); bound(); // 5 — bind pins 'this'
In PHP $this is permanently the object. In JavaScript this is determined by how the function is called, not where it was defined — so pulling a method off its object (const loose = counter.show) detaches this, and calling loose() fails. Fixes: call methods as obj.method(), .bind() the receiver, or use an arrow function (which has no this of its own). This trips up every PHP developer at least once.
Scope & Hoisting
<?php // PHP variables are function-scoped; no block scope. function demo(): void { if (true) { $message = "set inside the if"; } echo $message . "\n"; // works — still in scope } demo();
function demo() { if (true) { let message = "set inside the if"; } // console.log(message); // ReferenceError — block-scoped! console.log("let/const do not leak out of { }"); } demo();
PHP variables live for the whole function body regardless of the block they were set in. JavaScript's let and const are block-scoped: a variable declared inside an if or loop does not exist outside those braces. This is usually safer, but it is a real behavioral change — declare the variable in the outer scope if you need it there. (The legacy var is function-scoped like PHP, another reason to avoid it.)