Output & Running
Hello, World
Both languages run a file top to bottom with no entry-point function and no class wrapper, so the smallest program is the same size in each.
print("Hello, World!")console.log("Hello, World!");The difference is what happened before it ran. The Swift line was type-checked and compiled; the JavaScript line was parsed and executed. Everything else on this page follows from that, including the parts where JavaScript is genuinely more convenient. Semicolons are optional in both — Swift ignores them and JavaScript inserts them for you, which is occasionally not where you meant.
Printing several values
Interpolation exists in both with different punctuation, and only one of the two string quote characters supports it in JavaScript.
let name = "Ada"
let age = 36
print("\(name) is \(age)")
print("\(name) will be \(age + 1) next year")
print(name, "is", age)const name = "Ada";
const age = 36;
console.log(`${name} is ${age}`);
console.log(`${name} will be ${age + 1} next year`);
console.log(name, "is", age);A template literal uses backticks and
\${…}; an ordinary "…" or '…' string does not interpolate at all, so forgetting the backticks gives you the literal text rather than an error. console.log takes several arguments and joins them with a space, matching print. Number formatting is the awkward one in both: String(format: "%.2f", value) becomes value.toFixed(2), and there is no equivalent of a format specifier inside the interpolation.A compile step, and the absence of one
The edit-run loop is the thing an iOS developer notices first, and it is a genuine improvement.
// swift script.swift <- compiles, then runs
// swift build && swift run <- a package
// swift <- a REPL
print("Swift: compiled before anything happens")// node script.js <- parses, then runs
// node <- a REPL
// the browser <- also just runs it
console.log("JavaScript: no build step unless you added one");There is no compile, so a change is visible immediately, and the browser or a watcher reloads it for you — where a clean Xcode build of a mid-sized app is minutes. The cost is the one this whole page is about: nothing was checked, so a misspelled property name, a wrong argument count, and a value of entirely the wrong type all reach production and fail on the line that happens to use them. Most JavaScript teams buy some of that back with TypeScript, which is why
/swift/typescript exists; this page is about the runtime underneath it, which is the same either way.Async: The Convergence
async and await are nearly the same keywords
This is the convergence the page leads with. Swift borrowed
async/await from the same lineage JavaScript did, and at the surface the two are close to interchangeable.func fetch(_ name: String) async -> String {
try? await Task.sleep(nanoseconds: 10_000_000)
return "data for \(name)"
}
let value = await fetch("first")
print(value)const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchValue(name) {
await sleep(10);
return `data for ${name}`;
}
(async () => {
const value = await fetchValue("first");
console.log(value);
})();Both mark a suspending function
async and require await at every call to one, so the function-coloring problem is identical and a Swift developer's habits transfer straight over. Two mechanical notes for this page. Swift has no sleep that takes milliseconds, and JavaScript has no sleep at all — setTimeout wrapped in a promise is the idiom, and you will write it constantly. And every asynchronous example here is wrapped in an async IIFE, (async () => { … })(), because Node's -e mode has no top-level await. What differs underneath is the whole rest of this section.One thread and a job queue
Swift's concurrency runs on a cooperative thread pool with as many threads as there are cores. JavaScript has one thread and a queue of jobs, and this is the difference that changes how code is designed.
func work() async -> Int {
var total = 0
for value in 1...5_000_000 {
total &+= value
}
return total
}
// On a real thread pool this runs on some thread;
// other tasks keep making progress meanwhile.
let total = await work()
print(total > 0)function work() {
let total = 0;
for (let value = 1; value <= 5_000_000; value++) total += value;
return total;
}
(async () => {
const timer = setTimeout(() => console.log("timer fired"), 0);
const total = work(); // NOTHING else runs during this
console.log(total > 0);
clearTimeout(timer);
console.log("the timer never got a chance");
})();A synchronous loop in JavaScript blocks everything: no timer fires, no promise settles, no event is handled, and in a browser the page does not repaint or respond to a click.
await does not help — it yields at suspension points, and a tight loop has none. So CPU work goes to a Web Worker (or a worker_thread in Node), which is a separate JavaScript realm communicating by message passing, with no shared memory except a SharedArrayBuffer. That is closer to a Swift actor than to a Swift thread. The practical rule: in Swift, async buys concurrency and parallelism; in JavaScript it buys only concurrency.Task {} against a floating promise
Starting two pieces of work and awaiting both looks nearly identical, and the pieces have different names and different guarantees.
func fetch(_ name: String, delay: UInt64) async -> String {
try? await Task.sleep(nanoseconds: delay)
return "\(name) done"
}
async let first = fetch("first", delay: 30_000_000)
async let second = fetch("second", delay: 10_000_000)
let results = await [first, second]
print(results)const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchValue(name, delay) {
await sleep(delay);
return `${name} done`;
}
(async () => {
const first = fetchValue("first", 30);
const second = fetchValue("second", 10);
const results = await Promise.all([first, second]);
console.log(results);
})();async let is Promise.all over two already-started calls, since calling a JavaScript async function starts it immediately — the same as Swift, and unlike Rust's inert futures. The difference is structure. async let is a child task: it cannot outlive the scope that declared it, and if the scope exits early the child is cancelled. A JavaScript promise has no scope and no parent; nothing waits for it and nothing cancels it. Calling an async function and not awaiting the result gives you a floating promise that runs unobserved, and if it rejects you get an unhandled-rejection warning at best. Linters flag it (no-floating-promises) because the language will not.Cancellation against none
Swift builds cancellation into the concurrency model. JavaScript has no way to cancel a promise at all, and the standard workaround is a separate object you thread through by hand.
let task = Task {
for step in 1...5 {
try Task.checkCancellation()
try await Task.sleep(nanoseconds: 5_000_000)
print("step \(step)")
}
return "finished"
}
try? await Task.sleep(nanoseconds: 12_000_000)
task.cancel()
print("cancelled: \(task.isCancelled)")const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
(async () => {
const controller = new AbortController();
const work = (async (signal) => {
for (let step = 1; step <= 5; step++) {
if (signal.aborted) return "cancelled";
await sleep(5);
console.log(`step ${step}`);
}
return "finished";
})(controller.signal);
await sleep(12);
controller.abort();
console.log(await work);
})();A Swift
Task has cancel(), cancellation propagates to every child, and Task.checkCancellation() or Task.isCancelled is how a loop cooperates — the same cooperative model, with the plumbing supplied. In JavaScript a promise is a value representing a result that will arrive; there is nothing to cancel, because the work is not owned by the promise. AbortController is the convention: you create one, pass its signal to everything that should observe it, and each operation checks or listens. fetch and most modern APIs accept a signal; anything you wrote yourself has to check it itself, exactly as above.actor against "there is only one thread"
Swift needs a language construct to make shared mutable state safe, because its tasks genuinely run at the same instant. JavaScript needs nothing, because they cannot.
actor Counter {
private var value = 0
func increment() { value += 1 }
func current() -> Int { value }
}
let counter = Counter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<100 {
group.addTask { await counter.increment() }
}
}
print(await counter.current())(async () => {
let value = 0;
const increment = async () => {
value += 1; // safe: nothing else can run mid-statement
};
await Promise.all(Array.from({ length: 100 }, () => increment()));
console.log(value);
})();An
actor serializes access to its own state and the compiler enforces it by making every access from outside async — that is why the calls above need await. JavaScript gets the same serialization for free from the single thread: a synchronous run of statements cannot be interleaved, so value += 1 is atomic in the only sense that matters. The genuine hazard that replaces the data race is the interleaving bug: any await is a point where other code runs, so state you read before an await may be stale after it. Swift has exactly the same hazard — actor reentrancy — so this instinct transfers; what does not transfer is worrying about torn reads or memory ordering.AsyncSequence becomes an async generator
The consuming loop is spelled almost identically —
for await in both — and the producing side is markedly easier in JavaScript.func ticks(_ count: Int) -> AsyncStream<Int> {
AsyncStream { continuation in
Task {
for number in 0..<count {
try? await Task.sleep(nanoseconds: 5_000_000)
continuation.yield(number)
}
continuation.finish()
}
}
}
for await value in ticks(3) {
print(value * 10)
}const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function* ticks(count) {
for (let number = 0; number < count; number++) {
await sleep(5);
yield number;
}
}
(async () => {
for await (const value of ticks(3)) {
console.log(value * 10);
}
})();JavaScript has an async generator:
async function* with yield, which is a function containing the logic rather than a continuation you must remember to finish(). Swift has no async generator syntax, so producing a stream means AsyncStream and a continuation, and forgetting finish() hangs the consumer forever. Where Swift is ahead is the operator library: AsyncSequence has map, filter and prefix built in, while an async generator has no methods at all and you write the loop or reach for a library.Value Semantics, Gone
A struct copies; an object never does
This is the loss that costs an iOS developer the most bugs, and it is worth reading the two outputs before anything else on the page.
struct Point {
var x: Int
var y: Int
}
var first = Point(x: 1, y: 2)
var second = first
second.x = 99
print(first.x, second.x)const first = { x: 1, y: 2 };
const second = first;
second.x = 99;
console.log(first.x, second.x);Every "why did my array change" bug a Swift developer has been protected from is available again. A JavaScript object is a reference and assignment copies the reference, so
first and second are two names for one thing; arrays and every built-in container behave the same way. Note also that const is not let: it prevents reassignment of the binding and says nothing about the contents, so const first above still allowed the mutation. Swift's let on a struct freezes the whole value. The closest JavaScript comes is Object.freeze, which is shallow and silent — it ignores writes rather than rejecting them, unless the file is in strict mode.Copying, and how deep it goes
Making a copy is something you now have to do deliberately, and there are two kinds of copy with very different behavior.
struct Address { var city: String }
struct Customer { var name: String; var address: Address }
var original = Customer(name: "Ada", address: Address(city: "London"))
var copy = original
copy.address.city = "Paris"
print(original.address.city, copy.address.city)const original = { name: "Ada", address: { city: "London" } };
const shallow = { ...original };
shallow.address.city = "Paris";
console.log(original.address.city); // Paris — the nested object is shared
const deep = structuredClone(original);
deep.address.city = "Rome";
console.log(original.address.city, deep.address.city);Spread (
{ ...original }) and Object.assign are shallow: they copy the top-level properties, so any nested object is still shared and mutating through the copy is visible through the original. That is the trap, and it is the default. structuredClone, available in every modern runtime, is a genuine deep copy and handles cycles, Map, Set and dates — but not functions or class identity. A Swift struct copies deeply and for free because everything inside it is also a value; here you choose, per copy, and the cheap choice is the wrong one more often than not.Equality compares identity, not contents
A Swift struct conforming to
Equatable compares member by member. JavaScript's === on two objects asks only whether they are the same object.struct Point: Equatable {
var x: Int
var y: Int
}
let first = Point(x: 1, y: 2)
let second = Point(x: 1, y: 2)
print(first == second)
print(first.x == second.x && first.y == second.y)const first = { x: 1, y: 2 };
const second = { x: 1, y: 2 };
console.log(first === second);
console.log(first.x === second.x && first.y === second.y);The two columns are the same two lines and the first one disagrees, which is the whole row. There is no structural equality operator and no
Equatable to conform to — comparing two objects by value means comparing the fields yourself, or serializing both (which is order-dependent and silently wrong for anything non-JSON), or using a library such as lodash's isEqual. The consequence reaches further than it looks: array.includes(someObject), Set membership and Map keys all use identity, so two structurally identical objects are two different Set entries. Always use === rather than ==, which converts its operands first and produces results nobody defends.Getting immutability back, deliberately
Swift's
let on a struct freezes the whole value, deeply and at compile time. Everything JavaScript offers is a run-time approximation.struct Point { var x: Int; var y: Int }
let frozen = Point(x: 1, y: 2)
// frozen.x = 99 <- will not compile: let freezes the value
var mutable = frozen
mutable.x = 99
print(frozen.x, mutable.x)const frozen = Object.freeze({ x: 1, y: 2, nested: { z: 3 } });
frozen.x = 99; // silently ignored (throws in strict mode)
console.log(frozen.x);
frozen.nested.z = 99; // freeze is SHALLOW
console.log(frozen.nested.z);
const updated = { ...frozen, x: 99 };
console.log(frozen.x, updated.x);Object.freeze prevents adding, removing and changing top-level properties, and it is shallow, so a nested object is still mutable. It also fails silently outside strict mode — the assignment does nothing and no error appears, which is worse than either alternative; inside a module or a class body, which are always strict, it throws. The pattern the ecosystem settled on instead is convention: never mutate, always produce a new object with spread ({ ...frozen, x: 99 }), and let a linter enforce it. That is what React state and every Redux-shaped store require, and it is why a Swift developer's value-semantics instincts are a genuine asset in modern JavaScript even though the language does not help.Optionals become undefined
Optional becomes undefined AND null
Swift has one way to say "no value" and it is a different type from the value. JavaScript has two, they are not the same, and neither is checked.
func findName(_ id: Int) -> String? {
id == 1 ? "Ada" : nil
}
let name = findName(2)
print(name ?? "(nobody)")
let person: [String: String] = [:]
print(person["city"] ?? "unknown")function findName(id) {
return id === 1 ? "Ada" : null;
}
console.log(findName(2) ?? "(nobody)");
const person = {};
console.log(person.city ?? "unknown");
console.log(typeof person.city, typeof findName(2));undefined is what you get from a property that was never set, a missing argument, or a function with no return. null is a value someone assigned deliberately to mean "nothing here". The convention most codebases settle on is to produce null yourself and treat undefined as "absent", but nothing enforces it and JSON has only null, so both appear. Crucially there is no String?: the annotation is gone, the compiler is gone, and calling .length on either throws TypeError: Cannot read properties of undefined at the point of use — the error a Swift developer has not seen since they stopped writing Objective-C.?. and ?? exist in both and differ subtly
Both operators exist in both languages with the same spelling, and the JavaScript versions have two extra behaviors worth knowing before you rely on the instinct.
struct Address { var city: String? }
struct Customer { var address: Address? }
let customer = Customer()
print(customer.address?.city ?? "unknown")
let count: Int? = 0
print(count ?? -1) // 0 — ?? fires only on nil
print("Swift has no || that fires on a falsy value")const customer = {};
console.log(customer.address?.city ?? "unknown");
const count = 0;
console.log(count ?? "fallback"); // 0 — ?? only fires on null/undefined
console.log(count || "fallback"); // "fallback" — || fires on any falsy value
console.log(customer.getName?.());?. short-circuits the whole chain to undefined — note undefined, not null, whichever the left side was — and it works on calls (obj.method?.()) and indexes (array?.[0]) as well as properties, which Swift does not need because a missing method is a compile error. ?? is the nil-coalescing operator and fires only on null or undefined, so it matches Swift exactly. The trap is ||, which fires on any falsy value — 0, "", NaN, false — and was the only option before ?? shipped in 2020, so older code is full of defaults that trigger on a legitimate zero.guard let becomes an early return
The shape of a
guard let function survives — handle every absent case at the top, then read as straight-line code — and the binding half of it does not.func findName(_ id: Int) -> String? {
id == 1 ? "Ada" : nil
}
func shout(_ id: Int) -> String {
guard let name = findName(id) else {
return "(nobody)"
}
return name.uppercased()
}
print(shout(1))
print(shout(2))function findName(id) {
return id === 1 ? "Ada" : null;
}
function shout(id) {
const name = findName(id);
if (name == null) return "(nobody)";
return name.toUpperCase();
}
console.log(shout(1));
console.log(shout(2));There is no unwrapping, because there was no wrapper:
name is the value or it is null, and the if is a check rather than a bind. What is worth stealing is the == null comparison, which is the one place == is genuinely preferable to ===: loose equality treats null and undefined as equal to each other and to nothing else, so value == null is exactly "is this one of the two nothings". Writing if (!name) instead is the common mistake, because it also fires on "" and 0.Types at Run Time
One number type, and it is a Double
JavaScript has one numeric type and it is an IEEE 754 double, so
Int, Int64, Float and Double all collapse into it.let whole: Int = 7
let ratio: Double = 7 / 2
print(whole / 2)
print(ratio)
print(Int.max)
print(0.1 + 0.2)console.log(Math.trunc(7 / 2));
console.log(7 / 2);
console.log(Number.MAX_SAFE_INTEGER);
console.log(0.1 + 0.2);
console.log(9007199254740993);
console.log(9007199254740993n);Integer division must be written out (
Math.trunc(a / b) or a / b | 0), because / always produces a fraction. Integers are exact only up to Number.MAX_SAFE_INTEGER — 2⁵³−1, about 9 quadrillion — and past that they silently lose precision rather than overflowing, which is why the second-to-last line prints an even number. BigInt (the n suffix) is the arbitrary-precision escape hatch and does not mix with ordinary numbers in arithmetic. On the other side, there is no overflow trap: a Swift Int overflow crashes the process, and a JavaScript number simply becomes Infinity.Implicit conversion, which Swift has none of
Swift has no implicit numeric conversion at all — adding an
Int to a Double does not compile. JavaScript converts almost anything to almost anything, and the rules are not guessable.let count = 3
let label = "items"
print("\(count) \(label)")
print(Double(count) / 2)
// "3" + 3 does not compile.
// 3 + 3.0 does not compile either.console.log("3" + 3);
console.log("3" - 3);
console.log([] + {});
console.log(1 + true);
console.log("5" * "2");
console.log(Number("3") + 3);+ means concatenation if either side is a string and addition otherwise, so "3" + 3 is "33" while "3" - 3 is 0, because - has no string meaning and coerces instead. The defense is the same in every codebase: convert explicitly with Number(…), String(…) and parseInt(…, 10), and use === everywhere so comparison never coerces. This is the single most-mocked part of the language and it is also entirely avoidable in code you write; where it bites is code you did not write.is and as? become typeof and instanceof
Runtime type inspection exists in both, and JavaScript's tools are individually unreliable in ways worth memorizing.
func describe(_ value: Any) -> String {
if let text = value as? String {
return "text of \(text.count)"
}
if value is Int {
return "an integer"
}
return "something else"
}
print(describe("abc"))
print(describe(7))
print(describe(1.5))function describe(value) {
if (typeof value === "string") return `text of ${value.length}`;
if (Number.isInteger(value)) return "an integer";
if (Array.isArray(value)) return "an array";
return "something else";
}
console.log(describe("abc"));
console.log(describe(7));
console.log(describe(1.5));
console.log(typeof null, typeof []);typeof returns one of a small set of strings and famously reports "object" for null — a bug preserved since 1995 for compatibility — and also for arrays, which is why Array.isArray exists as a separate function. instanceof checks the prototype chain and works for classes, but fails across realms (an array from an iframe is not instanceof Array there). There is no as?, because there is nothing to cast: an object either has the property you are about to use or it does not, and asking is "name" in value or value.name !== undefined. That is duck typing, and the protocols section is where it really shows.Codable becomes JSON.parse, with nothing checked
This is where the loss of the type system is most concrete, because the data is arriving from somewhere you do not control.
import Foundation
struct Order: Codable {
let id: Int
let items: [String]
}
let order = Order(id: 7, items: ["pen", "ink"])
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(order)
print(String(data: data, encoding: .utf8)!)
let restored = try JSONDecoder().decode(Order.self, from: data)
print(restored.items[1])const order = { id: 7, items: ["pen", "ink"] };
const text = JSON.stringify(order);
console.log(text);
const restored = JSON.parse(text);
console.log(restored.items[1]);
// Nothing verified the shape. A malformed payload produces an
// object with holes, and the failure surfaces wherever it is read.
const wrong = JSON.parse('{"id": "seven"}');
console.log(wrong.items?.[1] ?? "items is missing");JSONDecoder validates against the declared type and throws on a missing or mistyped field, so a decoded value is a value you can trust. JSON.parse validates only that the text is JSON; the result is a plain object with whatever the server sent, and a missing field is undefined that surfaces as a TypeError three functions later. The idiomatic answer is a runtime validation library — Zod, Valibot, ArkType — which is a schema you declare and call, doing at run time what Codable does at compile time. Two smaller notes: JSON has no date type, so Date round-trips as an ISO string and needs a reviver; and JSON.stringify silently drops undefined values and functions.Date, and the API everybody warns you about
Date is one of the oldest parts of JavaScript and one of the most complained-about, and the reasons are worth knowing before you write a line of it.import Foundation
var components = DateComponents()
components.year = 2026
components.month = 8
components.day = 19
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "UTC")!
let moment = calendar.date(from: components)!
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.timeZone = TimeZone(identifier: "UTC")
print(formatter.string(from: moment))const moment = new Date(Date.UTC(2026, 7, 19)); // month is ZERO-BASED
console.log(moment.toISOString().slice(0, 10));
console.log(moment.getUTCMonth());
const later = new Date(moment);
later.setUTCDate(later.getUTCDate() + 10);
console.log(later.toISOString().slice(0, 10));
console.log(new Intl.DateTimeFormat("en-GB", {
dateStyle: "long", timeZone: "UTC",
}).format(moment));Months are zero-based and days are not, so
new Date(2026, 7, 19) is August. A Date is mutable, so setUTCDate changes the object in place and a shared one is a bug waiting to happen — hence the explicit copy above. And a Date is an instant with no time zone attached; the local zone leaks into every non-UTC method and into the string constructor, which parses some formats differently across runtimes. Intl.DateTimeFormat is the good part and is what DateFormatter corresponds to. The successor is Temporal, which brings immutable types with explicit calendars and zones — much closer to Foundation's model — and is shipping in runtimes now.Enums become conventions
Enums with payloads become an untagged convention
This is the sharpest structural loss on the page. There is no enum, no associated value, and no exhaustiveness — the pattern is an object with a
kind field and a switch nobody checks.enum FetchResult {
case success(String)
case failure(reason: String)
}
func render(_ result: FetchResult) -> String {
switch result {
case .success(let value):
return "ok: \(value)"
case .failure(let reason):
return "failed: \(reason)"
}
}
print(render(.success("data")))
print(render(.failure(reason: "timeout")))function render(result) {
switch (result.kind) {
case "success":
return `ok: ${result.value}`;
case "failure":
return `failed: ${result.reason}`;
default:
return "unknown";
}
}
console.log(render({ kind: "success", value: "data" }));
console.log(render({ kind: "failure", reason: "timeout" }));
console.log(render({ kind: "typo" }));The Swift version cannot compile with a case missing and cannot be handed a value that is neither. The JavaScript version compiles nothing, accepts
{ kind: "typo" } without complaint, and reaches the default at run time — and if you forget the default it returns undefined and fails somewhere else entirely. The discriminant field is conventionally called kind or type; nothing requires either. If the project uses TypeScript this becomes a real discriminated union with exhaustiveness checking, which is the single strongest argument for adopting it and is covered on /swift/typescript rather than here.A raw-value enum becomes a frozen object
For the simple case, the idiom is a frozen object of constants — and every property an enum gave you has to be rebuilt by hand.
enum Priority: Int, CaseIterable {
case low = 1
case high = 3
var label: String {
switch self {
case .high: return "urgent"
case .low: return "whenever"
}
}
}
for member in Priority.allCases {
print(member, member.rawValue, member.label)
}
print(Priority(rawValue: 3)!)const Priority = Object.freeze({ Low: 1, High: 3 });
const labelFor = (value) => (value === Priority.High ? "urgent" : "whenever");
for (const [name, value] of Object.entries(Priority)) {
console.log(name, value, labelFor(value));
}
console.log(Object.keys(Priority).find((key) => Priority[key] === 3));Object.freeze stops the constants being reassigned, which is the only guarantee available. Iteration is Object.entries rather than allCases; the reverse lookup that Priority(rawValue: 3) does is a find over the keys; and behavior attached to a case becomes a free function taking the value, since the value is a plain number with no identity of its own. Nothing prevents a caller passing 7 where a Priority was expected. The other common idiom is a bare string union — passing "high" directly — which is cheaper to write, prints readably, and offers exactly as much protection.Protocols & Prototypes
Protocols become duck typing
Swift asks whether a type declared that it satisfies a contract, before the program runs. JavaScript asks whether the object has the method, at the moment you call it.
protocol Speaker {
func speak() -> String
}
struct Duck: Speaker {
func speak() -> String { "quack" }
}
struct Robot: Speaker {
func speak() -> String { "beep" }
}
func chorus(_ things: [any Speaker]) -> String {
things.map { $0.speak() }.joined(separator: " ")
}
print(chorus([Duck(), Robot()]))const duck = { speak: () => "quack" };
const robot = { speak: () => "beep" };
const chorus = (things) => things.map((thing) => thing.speak()).join(" ");
console.log(chorus([duck, robot]));
const broken = { squeak: () => "eek" };
try {
chorus([broken]);
} catch (error) {
console.log(error.constructor.name);
}There is nothing to declare and nothing to conform to: an object with a
speak works, and one without it throws TypeError: thing.speak is not a function when the loop reaches it — which may be in production, on the third element, after two side effects have already happened. That is the whole trade. What duck typing buys is real flexibility: any object can be made to satisfy any expectation after the fact, no adapter type is needed, and there is no equivalent of the any Speaker versus some Speaker distinction because there is no static dispatch to choose. Testing is where it shows most — a fake is an object literal with the three methods the code touches.Classes are sugar over prototypes
The
class syntax arrived in 2015 and looks familiar enough to be misleading: underneath it is prototype delegation, not a class hierarchy.class Animal {
let name: String
init(name: String) { self.name = name }
func speak() -> String { "..." }
}
final class Dog: Animal {
override func speak() -> String { "woof" }
}
let animals: [Animal] = [Animal(name: "thing"), Dog(name: "rex")]
for animal in animals {
print(animal.name, animal.speak())
}class Animal {
#name;
constructor(name) { this.#name = name; }
get name() { return this.#name; }
speak() { return "..."; }
}
class Dog extends Animal {
speak() { return "woof"; }
}
for (const animal of [new Animal("thing"), new Dog("rex")]) {
console.log(animal.name, animal.speak());
}
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);A method lives on the prototype object, and an instance delegates to it at lookup time — so a method added to
Animal.prototype later is immediately available on every existing instance, which is the monkey-patching Swift extensions were designed to make safe. There is no override keyword, so a misspelled method name silently defines a new one rather than failing to compile, and no final. Fields prefixed with # are genuinely private — a real hard error from outside — where the older underscore convention was documentation. There is no protocol to implement and no multiple inheritance; the mixin idiom is copying methods onto a prototype.self is stable; this is not
In Swift,
self inside a method is the instance, always, and there is nothing further to learn. In JavaScript, this is decided by how the function was called, which is the single most confusing thing in the language.class Counter {
var value = 0
func increment() {
value += 1
}
func makeIncrementer() -> () -> Void {
return { self.increment() }
}
}
let counter = Counter()
let action = counter.makeIncrementer()
action()
action()
print(counter.value)class Counter {
value = 0;
increment() { this.value += 1; }
makeIncrementer() {
return () => this.increment(); // arrow: keeps this
}
}
const counter = new Counter();
const action = counter.makeIncrementer();
action();
action();
console.log(counter.value);
const detached = counter.increment;
try { detached(); } catch (error) { console.log(error.constructor.name); }Pull a method off an object and call it on its own — pass it as a callback, hand it to
setTimeout, use it as an event handler — and this is no longer the object; in a class body, which is strict mode, it is undefined and the call throws. The fix is either an arrow function, which has no this of its own and closes over the surrounding one (which is why makeIncrementer works above), or .bind(this), or defining the method as a class field holding an arrow. Arrow functions are the reason this problem is largely historical in new code, and knowing the rule is still necessary to read anything older.Extensions become prototype patching
Extending a type you do not own is possible in both, and in JavaScript it is a global mutation with no scope, which is why the community settled firmly against it.
extension String {
var isPalindrome: Bool {
let cleaned = lowercased().filter { $0 != " " }
return cleaned == String(cleaned.reversed())
}
}
extension Int {
func times(_ body: (Int) -> Void) {
for index in 0..<self { body(index) }
}
}
print("Never odd or even".isPalindrome)
3.times { print("tick \($0)") }// Adding to a built-in prototype WORKS and is strongly discouraged:
//
// String.prototype.isPalindrome = function () { ... };
//
// It is global, permanent, and collides with any other library
// or future language version that picks the same name.
const isPalindrome = (text) => {
const cleaned = [...text.toLowerCase()].filter((c) => c !== " ").join("");
return cleaned === [...cleaned].reverse().join("");
};
const times = (count, body) => {
for (let index = 0; index < count; index++) body(index);
};
console.log(isPalindrome("Never odd or even"));
times(3, (index) => console.log(`tick ${index}`));A Swift extension is resolved statically and is scoped by import, so two libraries adding
isPalindrome to String cannot collide. Patching String.prototype changes the string type for the entire program, permanently, including every library in it — and if a future JavaScript version adds a method with your name, your version silently wins and breaks code written against the standard one. That has happened; Array.prototype.flatten had to be renamed to flat because a popular library had taken the name. The accepted answer is a free function, which is why the right column reads the way it does.Computed properties and observers
Getters and setters exist in both and look almost the same. What does not survive is the observer that fires around a change to an ordinary stored property.
struct Rectangle {
var width: Int
var height: Int {
didSet { print("height changed from \(oldValue) to \(height)") }
}
var area: Int { width * height }
}
var rectangle = Rectangle(width: 3, height: 4)
print(rectangle.area)
rectangle.height = 10
print(rectangle.area)class Rectangle {
#height;
constructor(width, height) { this.width = width; this.#height = height; }
get height() { return this.#height; }
set height(value) {
console.log(`height changed from ${this.#height} to ${value}`);
this.#height = value;
}
get area() { return this.width * this.height; }
}
const rectangle = new Rectangle(3, 4);
console.log(rectangle.area);
rectangle.height = 10;
console.log(rectangle.area);A computed property maps directly onto a
get accessor, and a set accessor is the same idea with the incoming value named value rather than newValue. Swift's willSet and didSet have no counterpart: to observe a change here you must convert the field into a getter/setter pair with a private backing field, which is the shadow dance Swift removed. Note also that a getter is defined on the prototype, so it is shared by every instance, and that a plain object literal can carry one too ({ get area() { … } }). The Proxy object is the general-purpose interception mechanism, and it is far more powerful and far slower than any of this.Generics do not exist at all
There is no generic syntax, no type parameter and no constraint — and a function that works on anything comparable needs no annotation, because there were never any annotations.
func largest<T: Comparable>(_ items: [T]) -> T {
var result = items[0]
for item in items.dropFirst() where item > result {
result = item
}
return result
}
print(largest([3, 9, 2]))
print(largest(["fig", "apple"]))function largest(items) {
let result = items[0];
for (const item of items.slice(1)) {
if (item > result) result = item;
}
return result;
}
console.log(largest([3, 9, 2]));
console.log(largest(["fig", "apple"]));
console.log(largest([3, "apple"])); // no error, and no meaningDuck typing does the work generics do: the function uses
>, so it works on anything > works on, which is everything, because > coerces. That is why the third call returns an answer rather than an error, and the answer is meaningless — 3 > "apple" is false because the string converts to NaN. Swift's Comparable constraint is exactly what prevents that call from existing. There is also no some/any distinction and no monomorphization, because there is one compiled function and it does not know or care what it was given. TypeScript adds generics back at the type layer, which is /swift/typescript's subject rather than this page's.ARC becomes a Collector
No retain counts, no cycles to break
A whole discipline disappears here, and it is the one an iOS developer applies most often without thinking about it.
final class Node {
let name: String
var other: Node?
init(name: String) { self.name = name }
deinit { print("\(name) freed") }
}
func makeCycle() {
let first = Node(name: "first")
let second = Node(name: "second")
first.other = second
second.other = first // leaks: neither is ever freed
}
makeCycle()
print("scope ended; nothing was freed")class Node {
constructor(name) { this.name = name; this.other = null; }
}
function makeCycle() {
const first = new Node("first");
const second = new Node("second");
first.other = second;
second.other = first; // a cycle — and it does not matter
}
makeCycle();
console.log("scope ended; the collector will take both");JavaScript uses a tracing collector, so a cycle is collected as soon as nothing outside it is reachable. There is no
weak, no unowned, no [weak self] in a closure, and no retain cycle to find in Instruments — the entire category is gone. What you lose is deinit: there is no deterministic destructor, no hook that runs when an object dies, and nothing you can rely on to close a file or cancel a subscription. FinalizationRegistry exists and the specification explicitly does not guarantee it ever runs. So cleanup becomes an explicit method somebody has to call, which is a real regression from a language where deinit could not be forgotten.The leaks that do survive
A tracing collector removes cycles as a problem and does not remove leaks, which is the distinction worth being precise about.
final class Emitter {
var handlers: [() -> Void] = []
func on(_ handler: @escaping () -> Void) {
handlers.append(handler)
}
func fire() { handlers.forEach { $0() } }
}
final class Screen {
let emitter: Emitter
init(emitter: Emitter) {
self.emitter = emitter
emitter.on { [weak self] in
print("screen alive: \(self != nil)")
}
}
}
let emitter = Emitter()
_ = Screen(emitter: emitter)
emitter.fire()class Emitter {
handlers = [];
on(handler) { this.handlers.push(handler); }
fire() { for (const handler of this.handlers) handler(); }
}
class Screen {
constructor(emitter) {
this.name = "screen";
// The emitter now holds this closure, which holds this screen,
// FOREVER — the collector sees it as reachable.
emitter.on(() => console.log(`${this.name} alive`));
}
}
const emitter = new Emitter();
new Screen(emitter);
emitter.fire();
console.log("nothing here is collectable while emitter lives");Anything still reachable is kept, so a long-lived registry holding a closure that captures a short-lived object keeps that object alive indefinitely — the same defect a Swift retain cycle produces, with a different cause and no
[weak self] to fix it. The remedy is to unsubscribe: every addEventListener needs a matching removeEventListener, every interval a clearInterval, every subscription an unsubscribe. WeakMap, WeakSet and WeakRef exist for the cases where you genuinely want a reference that does not keep its target alive, and a WeakMap keyed on an object is the idiomatic way to attach data to something without extending its lifetime.Strings
Counting characters: grapheme clusters against code units
This is one line in each column, prints wildly different numbers, and teaches something true about both languages.
let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}"
let flag = "\u{1F1EC}\u{1F1E7}"
print(family.count)
print(flag.count)
print("café".count)const family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
const flag = "\u{1F1EC}\u{1F1E7}";
console.log(family.length);
console.log(flag.length);
console.log("café".length);
console.log([...family].length);
console.log([...new Intl.Segmenter().segment(family)].length);Swift counts extended grapheme clusters — everything a reader would call one character — so a three-person family emoji is 1 and a flag is 1. JavaScript's
length counts UTF-16 code units, so the same family string is 8: three emoji at two units each, plus two zero-width joiners. Spreading the string ([...family]) iterates by code point and gives 5. Getting Swift's answer needs Intl.Segmenter, which is standard in modern runtimes and which nobody remembers exists. The practical consequence: any string truncation written against length will eventually cut an emoji in half.String operations, and the indexes you get back
The operations line up one for one with shorter names, and the thing a Swift developer will actually enjoy is that strings can be indexed by integer.
import Foundation
let sentence = " the quick brown fox "
let trimmed = sentence.trimmingCharacters(in: .whitespaces)
print(trimmed)
print(trimmed.uppercased())
print(trimmed.split(separator: " "))
print(trimmed.replacingOccurrences(of: "quick", with: "slow"))
print(sentence.contains("fox"))const sentence = " the quick brown fox ";
const trimmed = sentence.trim();
console.log(trimmed);
console.log(trimmed.toUpperCase());
console.log(trimmed.split(" "));
console.log(trimmed.replace("quick", "slow"));
console.log(sentence.includes("fox"));
console.log(trimmed.slice(4, 9));There is no
String.Index: text[3] and text.slice(4, 9) work directly, because a JavaScript string is a fixed-width array of UTF-16 units and the third one is at a known offset. That is exactly what Swift gives up in exchange for the grapheme guarantee in the previous row, and it is why slicing here is convenient and occasionally cuts a character in half. Note also that replace with a string argument replaces the first occurrence only — replaceAll, or a regular expression with the g flag, does what replacingOccurrences does.Regular expressions are a literal
A regular expression is a first-class literal in JavaScript, written between slashes, and it has been since 1995 — where Swift only gained regex literals in 5.7.
import Foundation
let log = "2026-08-19 ERROR disk full"
let pattern = try! Regex(#"^(\d{4})-(\d{2})-(\d{2}) (\w+)"#)
if let match = log.firstMatch(of: pattern) {
print(match[1].substring ?? "", match[4].substring ?? "")
}
print("too many spaces".replacingOccurrences(
of: " +", with: " ", options: .regularExpression))const log = "2026-08-19 ERROR disk full";
const match = log.match(/^(\d{4})-(\d{2})-(\d{2}) (\w+)/);
if (match) console.log(match[1], match[4]);
console.log("too many spaces".replace(/ +/g, " "));
console.log([..."a1b22c333".matchAll(/\d+/g)].map((m) => m[0]));
const { year } = "2026-08".match(/(?<year>\d{4})/).groups;
console.log(year);The flags go after the closing slash:
g for every match, i for case-insensitive, m for multi-line, u/v for Unicode. g is the one to watch, because a regex object with it carries a mutable lastIndex and reusing the same object across calls gives different answers — which is why matchAll exists and requires g. Captures come back as an array-like match object with the whole match at index 0, and named groups ((?<year>…)) land in .groups, which is the closest thing to Swift's typed regex output. What Swift has and JavaScript does not is RegexBuilder and compile-time-checked capture types.Collections
An array is an object with numeric keys
A Swift array is a value type with bounds checking that traps. A JavaScript array is an object whose keys happen to be numeric strings, and it has neither property.
var numbers = [1, 2, 3]
numbers.append(4)
print(numbers)
print(numbers.count)
// numbers[10] would trap.
print(numbers.indices.contains(10))const numbers = [1, 2, 3];
numbers.push(4);
console.log(numbers);
console.log(numbers.length);
console.log(numbers[10]); // undefined, not a crash
numbers[10] = 99;
console.log(numbers.length, numbers);
console.log(typeof numbers);Reading past the end gives
undefined rather than crashing, which turns a loud failure into a quiet one that surfaces three functions later. Writing past the end extends the array and fills the gap with empty slots, so length becomes 11 and iteration sees holes. Assigning to length truncates. Because it is an object, an array can also carry non-numeric properties, which is legal and always a mistake. The methods themselves are good — map, filter, reduce, find, some, every, flatMap all match Swift's — and the newer toSorted, toReversed and with return copies rather than mutating, which is worth preferring for the reason the value-semantics section gave.Dictionary becomes an object or a Map
There are two options and the older one is used far more often than it should be.
var ages = ["Ada": 36, "Bo": 17]
ages["Cy"] = 44
print(ages["Ada"] ?? 0)
print(ages["Nobody"] ?? 0)
print(ages.count)
print(ages.keys.sorted())const ages = { Ada: 36, Bo: 17 };
ages.Cy = 44;
console.log(ages.Ada ?? 0);
console.log(ages.Nobody ?? 0);
console.log(Object.keys(ages).length);
console.log(Object.keys(ages).sort());
const map = new Map([["Ada", 36]]);
map.set("Bo", 17);
console.log(map.get("Ada"), map.size);A plain object is the ubiquitous choice and has real problems as a dictionary: its keys are strings only (a number key is silently converted), it inherits properties from
Object.prototype so ages.toString is a function rather than undefined, and counting entries needs Object.keys(…).length. Map is what a Swift Dictionary actually corresponds to — any value as a key, a real size, guaranteed insertion order, and no inherited keys — and it is underused because object literals are so convenient. Both give undefined for a missing key rather than trapping, which matches Swift's optional subscript. Object key order is mostly insertion order, except that integer-like keys are sorted first, so never rely on it.map, filter and reduce with the same names
The three functions share their names, their argument order and their behavior, so this is the most transferable knowledge on the page — with one difference in laziness.
struct Person { let name: String; let age: Int }
let people = [Person(name: "Ada", age: 36),
Person(name: "Bo", age: 17),
Person(name: "Cy", age: 44)]
let names = people.filter { $0.age >= 18 }.map { $0.name.uppercased() }
print(names)
print(people.reduce(0) { $0 + $1.age })
print(people.first { $0.age < 18 }?.name ?? "none")const people = [
{ name: "Ada", age: 36 },
{ name: "Bo", age: 17 },
{ name: "Cy", age: 44 },
];
const names = people.filter((p) => p.age >= 18).map((p) => p.name.toUpperCase());
console.log(names);
console.log(people.reduce((total, person) => total + person.age, 0));
console.log(people.find((person) => person.age < 18)?.name ?? "none");Both are eager and build an intermediate array at each step. Swift's
lazy makes a chain lazy; JavaScript has no equivalent on arrays, and the lazy option is an iterator or generator, which has none of these methods attached — so a lazy chain means writing a generator or reaching for a library. first(where:) is find, contains(where:) is some, allSatisfy is every, and compactMap is flatMap with the nils filtered, or map then filter(Boolean). reduce takes its initial value last in JavaScript, and omitting it makes the first element the seed, which fails on an empty array.Set, and what makes a thing iterable
Set exists with different method names, and the protocol that makes a type usable in a for loop is a well-known method name rather than a declared conformance.let unique: Set<Int> = [1, 2, 2, 3]
print(unique.count)
print(unique.contains(2))
print(unique.sorted())
struct Countdown: Sequence, IteratorProtocol {
var remaining: Int
mutating func next() -> Int? {
guard remaining > 0 else { return nil }
defer { remaining -= 1 }
return remaining
}
}
print(Array(Countdown(remaining: 3)))const unique = new Set([1, 2, 2, 3]);
console.log(unique.size);
console.log(unique.has(2));
console.log([...unique].sort());
function countdown(from) {
return {
*[Symbol.iterator]() {
for (let remaining = from; remaining > 0; remaining--) yield remaining;
},
};
}
console.log([...countdown(3)]);count is size, contains is has, and insert is add. The membership caveat from the equality section applies: a Set compares with ===, so two structurally identical objects are two entries. Conforming to Sequence becomes defining a method named [Symbol.iterator] that returns an iterator — and the shortest way to write one is a generator, function* with yield, which the compiler turns into the state machine that IteratorProtocol makes you write by hand. Anything with that method works in for…of, spreads with ..., and destructures.Destructuring, by name as well as position
Destructuring is one of the places JavaScript is plainly more capable, and an iOS developer picks it up in an afternoon and then misses it going back.
let point = (x: 3, y: 4)
let (x, y) = (point.x, point.y)
print(x, y)
struct Person { let name: String; let age: Int }
let person = Person(name: "Ada", age: 36)
// There is no struct destructuring: the fields are read by name.
let name = person.name
let years = person.age
print(name, years, "unknown")
let numbers = [1, 2, 3, 4]
let first = numbers.first ?? 0
let rest = Array(numbers.dropFirst())
print(first, rest)const point = { x: 3, y: 4 };
const { x, y } = point;
console.log(x, y);
const person = { name: "Ada", age: 36 };
const { name, age: years = 0, city = "unknown" } = person;
console.log(name, years, city);
const [first, ...rest] = [1, 2, 3, 4];
console.log(first, rest);It works on objects by property name, with renaming (
age: years) and per-field defaults (city = "unknown") — none of which Swift can do for a struct, where destructuring is limited to tuples. On arrays it is positional with a rest element (...rest). It also works in a parameter list, which is what the options-object pattern in the functions section relies on, and it nests. The one trap: a default fires only on undefined, never on null, so { city = "unknown" } leaves an explicit null alone.Control Flow & guard
switch falls through, and checks nothing
The keyword is the same and almost nothing else is. JavaScript's
switch is C's: it compares with ===, it falls through unless you break, and it is a statement rather than an expression.func describe(_ value: Int) -> String {
switch value {
case 0:
return "zero"
case 1...9:
return "single digit"
case let n where n < 0:
return "negative \(n)"
default:
return "large"
}
}
print(describe(0))
print(describe(5))
print(describe(-3))function describe(value) {
switch (true) {
case value === 0:
return "zero";
case value >= 1 && value <= 9:
return "single digit";
case value < 0:
return `negative ${value}`;
default:
return "large";
}
}
console.log(describe(0));
console.log(describe(5));
console.log(describe(-3));There are no range patterns, no
where clauses, no tuple patterns and no binding — the switch (true) idiom above is how you get arbitrary conditions in, and it is standard rather than a hack. There is also no exhaustiveness: leaving out a case is silence, and forgetting break in a non-returning arm runs the next one too, which is a genuine and frequent bug. Most JavaScript in this shape is written as an if/else if chain or an object lookup (const label = { 0: "zero" }[value] ?? "large") instead, and both read better than the switch.for-in, and the three loops that are not it
The loop you want is
for…of, and JavaScript has two others with confusingly similar names that do something else.let words = ["fig", "apple", "pear"]
for (index, word) in words.enumerated() {
print(index, word)
}
for number in stride(from: 0, to: 6, by: 2) {
print(number)
}
let ages = ["Ada": 36]
for (name, age) in ages {
print(name, age)
}const words = ["fig", "apple", "pear"];
for (const [index, word] of words.entries()) {
console.log(index, word);
}
for (let number = 0; number < 6; number += 2) {
console.log(number);
}
const ages = { Ada: 36 };
for (const [name, age] of Object.entries(ages)) {
console.log(name, age);
}for…of iterates values and is the direct counterpart of for…in in Swift. JavaScript's for…in iterates the keys of an object, including inherited ones, and using it on an array gives you string indexes and any stray properties — it is almost never what you want. The C-style for (let i = …) is still the way to write a strided or descending loop, since there is no stride. entries() is enumerated(), and destructuring in the loop head works the same way as in Swift.Truthiness, which Swift does not have
Swift requires a condition to be a
Bool and nothing else. JavaScript converts anything, and the list of what counts as false is short and worth memorizing outright.let values: [Any?] = [0, 1, "", "0", [Int](), nil]
for value in values {
// Swift has no truthiness: a condition MUST be a Bool, so
// there is no second column to print. if 0 { } does not compile.
print(String(describing: value))
}for (const value of [0, 1, "", "0", [], null, undefined, NaN]) {
console.log(JSON.stringify(value), Boolean(value));
}Exactly six values are falsy:
false, 0 (and -0, and 0n), "", null, undefined and NaN. Everything else is truthy — including "0", "false", [] and {}, the last two of which surprise people who expect an empty container to be falsy the way it is in Python or Ruby. The practical hazard for a Swift developer is if (count) as a presence check, which treats a legitimate zero as absent; the fix is the explicit if (count != null) from the optionals section.Breaking out of a nested loop
Labeled statements exist in JavaScript with the same syntax and the same meaning, which is unusual enough among the two languages' control flow to be worth a row of its own.
let rows = [[1, 2], [3, 4]]
let target = 4
outer: for (rowIndex, row) in rows.enumerated() {
for (columnIndex, value) in row.enumerated() {
if value == target {
print(rowIndex, columnIndex)
break outer
}
}
}const rows = [[1, 2], [3, 4]];
const target = 4;
outer: for (const [rowIndex, row] of rows.entries()) {
for (const [columnIndex, value] of row.entries()) {
if (value === target) {
console.log(rowIndex, columnIndex);
break outer;
}
}
}A label is an identifier followed by a colon in front of a loop, and
break label or continue label targets it — identical to Swift. JavaScript also allows a label on a plain block, so break can jump out of a bare { }, which Swift does not. The reason to know this is that the alternatives are worse: Array.prototype.forEach cannot be broken out of at all (a return inside it is a continue), so a search over nested data written with forEach has to fall back to a flag variable. Prefer for…of whenever a loop might need to stop early.Functions & Argument Labels
Argument labels become an options object
Swift's labels are part of a function's identity and are mandatory at the call site. JavaScript has no named arguments at all, and the substitute is a destructured object.
func move(_ target: String, from origin: String, to destination: String) -> String {
"\(target): \(origin) -> \(destination)"
}
print(move("piece", from: "e2", to: "e4"))function move(target, { from, to }) {
return `${target}: ${from} -> ${to}`;
}
console.log(move("piece", { from: "e2", to: "e4" }));
const moveAll = (target, ...squares) => `${target}: ${squares.join(" -> ")}`;
console.log(moveAll("piece", "e2", "e4", "e5"));The options-object pattern gets you readable call sites and order independence, and it is the standard idiom for any function with more than two or three parameters — including default values, written
{ from = "a1", to } in the parameter list. What it does not get you is any check: pass an object missing to and the parameter is undefined, pass a misspelled too and nothing complains. Calling a function with the wrong number of arguments is also legal — extras are ignored, missing ones are undefined — which is the arity check a Swift developer will most miss. Rest parameters (...squares) are variadics and are the one place JavaScript is tidier.inout has no counterpart, because everything is shared
inout exists in Swift precisely because arrays are values. Here they are references, so a callee can always modify what it was given, and there is no marker at the call site to warn you.func scale(_ values: inout [Int], by factor: Int = 2) {
for index in values.indices {
values[index] *= factor
}
}
var numbers = [1, 2, 3]
scale(&numbers)
print(numbers)function scale(values, factor = 2) {
for (let index = 0; index < values.length; index++) {
values[index] *= factor; // the caller's array, no & required
}
}
const numbers = [1, 2, 3];
scale(numbers);
console.log(numbers);
function reassign(values) { values = [9]; }
reassign(numbers);
console.log(numbers); // unchanged: the BINDING was localReading the JavaScript call, you cannot tell whether
numbers will change — the information Swift puts in both the signature and the & at the call is simply absent. The distinction that remains is the second example: reassigning the parameter rebinds a local name and the caller sees nothing, while mutating the object it points at is visible everywhere. Default parameter values work the same way in both, and JavaScript evaluates them on every call — left to right, so a later default may refer to an earlier parameter.Closures
Trailing closures become arrow functions
Closures are first class in both, and the trailing-closure syntax that makes Swift libraries read like language constructs has no counterpart here.
let numbers = [3, 1, 2]
print(numbers.sorted { $0 > $1 })
func applyTwice(_ function: (Int) -> Int, to value: Int) -> Int {
function(function(value))
}
print(applyTwice({ $0 + 1 }, to: 5))
print(applyTwice({ value in value * 2 }, to: 5))const numbers = [3, 1, 2];
console.log(numbers.toSorted((left, right) => right - left));
const applyTwice = (fn, value) => fn(fn(value));
console.log(applyTwice((value) => value + 1, 5));
console.log(applyTwice((value) => value * 2, 5));An arrow function is the modern form:
(a, b) => expression returns the expression with no return, and a braced body needs one. There is no $0 shorthand, so every parameter is named. Everything goes inside the parentheses, so DispatchQueue.main.async { … }-style APIs read as setTimeout(() => { … }, 0). The comparator is the detail to carry over: JavaScript's sort takes a function returning a number (negative, zero, positive) rather than a Bool, and its default with no comparator sorts as strings — so [10, 9].sort() gives [10, 9]. Prefer toSorted over sort, which mutates in place.Capture, with no capture list
Both languages capture variables rather than values by default, and only Swift gives you a way to say otherwise.
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter(), counter())
var index = 0
let capturedByValue = { [index] in index }
index = 99
print(capturedByValue(), index)function makeCounter() {
let count = 0;
return () => {
count += 1;
return count;
};
}
const counter = makeCounter();
console.log(counter(), counter());
const functions = [];
for (let i = 0; i < 3; i++) functions.push(() => i);
console.log(functions.map((f) => f()));
const withVar = [];
for (var j = 0; j < 3; j++) withVar.push(() => j);
console.log(withVar.map((f) => f()));There is no capture list, so
[index] and [weak self] have no equivalent — copying a value means assigning it to a new local before the closure is created. Nor is there any need for weak, since the collector handles cycles. The one thing to know is the loop: let in a for head creates a fresh binding per iteration, so the three closures capture 0, 1 and 2 as a Swift developer expects, while the older var is function-scoped and all three see the final value. That was the most famous JavaScript interview question for a decade and let fixed it in 2015; the reason to know it is reading older code.Error Handling
throws disappears from the signature
The two things Swift puts in the source —
throws in the signature and try at every call that can fail — both disappear.enum ParseError: Error {
case notANumber(String)
}
func readPort(_ text: String) throws -> Int {
guard let value = Int(text) else {
throw ParseError.notANumber(text)
}
return value
}
do {
print(try readPort("8080"))
print(try readPort("eighty"))
} catch ParseError.notANumber(let text) {
print("not a number: \(text)")
}class ParseError extends Error {
constructor(text) {
super(`not a number: ${text}`);
this.name = "ParseError";
this.text = text;
}
}
function readPort(text) {
const value = Number(text);
if (!Number.isInteger(value)) throw new ParseError(text);
return value;
}
try {
console.log(readPort("8080"));
console.log(readPort("eighty"));
} catch (error) {
if (error instanceof ParseError) console.log(error.message);
else throw error;
}A JavaScript function that can throw looks exactly like one that cannot, and reading a block you cannot tell which lines might fail.
catch takes no type, so a single handler receives everything and dispatching is a chain of instanceof checks — with the discipline that anything you did not expect must be re-thrown, or you have silently swallowed a programming error. Worse, throw accepts any value: a string, a number, undefined. Always throw an Error (or a subclass), because only those carry a stack trace, and error.cause is the modern way to chain one failure to another.Async errors, and the ones nobody catches
Inside an
async function, try/catch works on awaited calls exactly as it does in Swift — and there is a second failure mode with no Swift counterpart.enum FetchError: Error { case failed }
func fetch(_ shouldFail: Bool) async throws -> String {
if shouldFail { throw FetchError.failed }
return "data"
}
do {
print(try await fetch(false))
print(try await fetch(true))
} catch {
print("caught: \(error)")
}async function fetchValue(shouldFail) {
if (shouldFail) throw new Error("failed");
return "data";
}
(async () => {
try {
console.log(await fetchValue(false));
console.log(await fetchValue(true));
} catch (error) {
console.log(`caught: ${error.message}`);
}
// Not awaited, not caught: an unhandled rejection.
fetchValue(true).catch((error) => console.log(`rejected: ${error.message}`));
})();A rejected promise that nothing is awaiting or
.catch()ing becomes an unhandled rejection: in Node it terminates the process by default, and in a browser it logs a warning nobody reads. That cannot happen in Swift, because a Task whose result is discarded still cannot throw past its boundary, and an unawaited async let is a compile error. The rule is that every promise must have an owner — awaited, returned, or explicitly given a .catch. Promise.allSettled is the tool when you want every result regardless of failures, and finally works on both a try block and a promise chain.defer becomes finally
defer registers cleanup next to the thing it undoes. JavaScript has only finally, which puts it at the bottom of the block instead.func work() {
defer { print("cleanup runs last") }
print("working")
}
work()
func nested() {
defer { print("second") }
defer { print("first") }
print("body")
}
nested()function work() {
try {
console.log("working");
} finally {
console.log("cleanup runs last");
}
}
work();
// No defer: cleanup must be written at the bottom, in a finally,
// and several cleanups nest rather than stacking.
console.log("no equivalent of two defers in one scope");For one cleanup the two are equivalent, and
finally runs on every exit path including a return from inside the try. Where they diverge is several cleanups in one scope: Swift stacks them and runs them in reverse, while JavaScript needs nested try/finally blocks and the indentation that implies. There is also no using-style scoped resource, though the explicit-resource-management proposal (using and Symbol.dispose) is landing in runtimes now and will be the closest equivalent to a Swift deinit-plus-scope that the language has ever had.Packages & Tooling
Modules: two systems, and one of them is why this page uses IIFEs
JavaScript has two module systems that coexist uneasily, and the difference is why every asynchronous example on this page is wrapped in an IIFE rather than using top-level
await.// Every file in a target shares one namespace:
// no import between them, and access control
// (private, fileprivate, internal, public) does
// the encapsulating.
import Foundation
print(sqrt(16.0))// ESM — the standard, used in browsers and modern Node:
// import { readFile } from "node:fs/promises";
// export function helper() {}
//
// CommonJS — Node's original, still everywhere:
// const { readFile } = require("node:fs/promises");
// module.exports = { helper };
//
// A file exports NOTHING unless it says so — the opposite of Swift.
console.log(Math.sqrt(16));ESM (
import/export) is the standard, supports top-level await, and is what a browser loads. CommonJS (require) is Node's original, is synchronous, does not support top-level await — and is what node -e runs, which is the constraint this page is written under. The visibility model is the reverse of Swift's: a Swift file shares its target's namespace and hides what is private, while a JavaScript module exposes nothing unless exported, and every file is its own scope. Which system a package uses is decided by "type": "module" in package.json and by the .mjs/.cjs extensions.Swift Package Manager against npm
Both tools do the same job with the same split between declared ranges and a resolved lockfile, and the ecosystems around them could hardly be less alike.
// Package.swift
// dependencies: [
// .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0")
// ]
//
// swift build -> Package.resolved pins exact commits
// A dependency is a git URL; there is no registry.
print("Swift: a manifest that is itself Swift, and a resolved file")// package.json
// "dependencies": { "lodash": "^4.17.21" }
//
// npm install -> package-lock.json pins the whole tree
// npm run build
//
// A registry with ~3 million packages, and deep transitive trees.
console.log("JavaScript: a registry, a lockfile, and node_modules");package.json is Package.swift and package-lock.json is Package.resolved; ^4.17.21 is the from: range. The differences are cultural and consequential. npm has a central registry rather than git URLs, which makes publishing trivial and name-squatting and typosquatting real supply-chain risks. Dependency trees are far deeper — a modest project easily pulls a thousand transitive packages, where a Swift one pulls a handful — so npm audit and lockfile review are routine work rather than an occasional check. On the other hand there is no build step to wait for, and npx runs a package without installing it.Tests
Node has had a built-in test runner since version 18, and most projects still use a third-party one — which is the reverse of the Swift situation, where the built-in runner is what everybody uses.
// Tests/BillingTests/BillingTests.swift, run with: swift test
//
// import Testing
//
// @Test func addsTwoNumbers() {
// #expect(add(2, 3) == 5)
// }
func add(_ left: Int, _ right: Int) -> Int { left + right }
print(add(2, 3))// billing.test.js, run with: node --test (or vitest / jest)
//
// import { test } from "node:test";
// import assert from "node:assert/strict";
//
// test("adds two numbers", () => {
// assert.equal(add(2, 3), 5);
// });
const add = (left, right) => left + right;
console.log(add(2, 3));The shapes are close: a test is a function with a name, and assertions are function calls.
node:assert/strict uses ===-style comparison and deepStrictEqual for structures, which matters because of the identity-equality problem from earlier — comparing two objects with assert.equal compares references and passes only when they are the same object. Vitest and Jest are the common choices and bring watch mode, mocking and snapshot testing. Tests live beside the code (billing.test.js next to billing.js) rather than in a parallel tree, and they see whatever the module exports and nothing else.