Skip to content

JavaScript and TypeScript

The event loop, closures, coercion, and what TypeScript actually checks.

01

The event loop

beginner

JavaScript runs your code on one thread. Everything else, timers, network responses, file reads, user events, happens elsewhere and is handed back as a task to run when the thread is free. The event loop is the mechanism: run the current task to completion, then take the next one.

The rule that follows is that nothing else happens while your code runs. A loop that takes 300 milliseconds blocks rendering, input handling and every pending callback for 300 milliseconds. On a page, that is a frozen interface; in Node, that is every concurrent request waiting. This is the single most important operational fact about the language.

Not all queued work is equal. Promises resolve on the microtask queue, which is drained completely after the current task and before the next one, while setTimeout schedules a macrotask that waits its turn. So a promise chain that never awaits anything real can starve the timer queue, and a setTimeout with zero delay still runs after every pending promise callback.

Async and await do not add threads. They mark the points where a function may pause and let the loop run something else, then resume. That is why an await inside a loop serialises the whole loop, and why Promise.all is the difference between ten sequential requests and ten concurrent ones. The work is still on one thread; only the waiting overlaps.

Check yourself

A 300ms synchronous loop runs in a Node request handler. What is affected?

drained first, completelythen one macrotaskthen the browser paintsa long task blocks all of itCurrent taskruns to completionMicrotaskspromise callbacksMacrotaskstimers, IORenderblocked until theloop is free
ServiceQueue / streamEdge / CDNDashed = asynchronous
One thread, two queues, and everything else waiting
Evidence · 2 sources
  1. 01
    Node.js: the event loop, timers and process.nextTick

    The loop phases and the ordering of microtasks against timers, which is why a resolved promise's callback runs before a zero-delay timeout.

  2. 02
    MDN: Long Tasks API

    That work holding the main thread for more than 50ms is reported as a long task, which is the threshold at which interaction starts to feel broken.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

02

Closures, prototypes and this

intermediate

A closure is a function plus the variables it captured where it was defined, and it is the mechanism behind most JavaScript patterns: callbacks that remember context, module privacy, function factories, hooks. Understanding that the function holds the variable rather than its value at the time explains both the power and the classic loop bug that let was introduced to fix.

Objects inherit through a prototype chain rather than through classes. A property lookup walks from the object to its prototype and onwards until it finds the name or runs out, and class syntax is a more familiar spelling of exactly that. Knowing the chain exists explains why adding a method to a prototype affects every existing instance, and why a property that shadows one further up hides rather than replaces it.

Then there is this, which is bound by how a function is called rather than where it is defined. Extract a method from an object and call it on its own and this is no longer that object, which is the source of the callback that mysteriously stops working. Arrow functions do not bind their own this, taking it from the enclosing scope, which is why they are the default inside callbacks and the wrong choice for an object method that needs the receiver.

The practical rules: use arrow functions for callbacks, use regular functions or classes for methods that need this, and prefer passing values explicitly over relying on binding. Most this-related bugs disappear when the code stops depending on how a function will be called later.

Check yourself

A method is extracted from an object and passed as a callback, and this becomes undefined. Why?

passed as a callbackcalled with no receiverwritten inside the methodcaptured, not boundobj.method()receiver is objf = obj.methodreceiver lost() => this.xthis from enclosingscopethis is undefinedWorks in a callback
ClientServiceExternalData storeDashed = asynchronousConsidered, not chosen
this comes from the call site; a closure comes from where it was written
Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

03

Equality, coercion and the sharp edges

intermediate

JavaScript will convert types to make a comparison work, and the rules are more elaborate than anyone can hold in their head. The practical answer is to use strict equality, which compares without converting, and to convert deliberately when you mean to. Almost every surprising comparison in the language comes from the loose operator being allowed to guess.

The specific facts worth memorising are short. NaN is not equal to itself, which is why isNaN and Number.isNaN exist. typeof null returns object, a bug preserved since 1995 for compatibility. An empty array is truthy on its own and still equal to false and to zero under loose comparison, because the loose operator converts it to an empty string and then to a number, so the same value answers yes and no depending on how it is asked. Adding a number to a string concatenates, and subtracting converts, so the same two values produce a string with one operator and a number with another.

Falsy values are a fixed set worth knowing exactly: false, 0, minus 0, 0n, the empty string, null, undefined and NaN. Everything else is truthy, including empty arrays and empty objects, which is why checking a response by truthiness rather than by a property is a reliable way to accept something empty as success. That is also why the previous paragraph is not a contradiction: an empty array is truthy under if, and equal to false under the loose operator, because those are two different questions with two different conversion rules.

Modern syntax removes most of the remaining traps. Optional chaining reads a nested property without throwing when something in the middle is missing, and nullish coalescing supplies a default only for null and undefined rather than for every falsy value, so a legitimate zero or empty string is no longer replaced by a fallback. That last distinction fixes a whole category of quiet bugs in configuration handling.

Check yourself

Why does NaN not equal itself?

config.retriesthe value is 0value || 3any falsy is replacedvalue ?? 3only null orundefined3the zero wasdiscarded0the setting ishonoured
ClientServiceExternalData storeDashed = asynchronousConsidered, not chosen
Two operators, two very different answers for a missing value
Evidence · 2 sources
  1. 01
    MDN: falsy

    The complete falsy set quoted here, false, 0, minus 0, 0n, the empty string, null, undefined and NaN, which is what establishes that an empty array is truthy.

  2. 02
    MDN: nullish coalescing operator

    That it supplies a default only for null and undefined rather than for every falsy value, which is why a legitimate zero or empty string is no longer replaced.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

04

TypeScript is structural, and it disappears

advanced

TypeScript checks shapes rather than names. If an object has the properties a type requires, it satisfies that type, whether or not anyone declared a relationship. That is structural typing, and it is why you can pass an object literal to a function expecting an interface without implementing anything, and why two identically shaped types from different libraries are interchangeable.

The second fact is that all of it is erased. Types exist during compilation and produce no runtime code at all, so nothing checks a value at the boundary of the program unless you write that check. An API response cast as a User is a User as far as the compiler is concerned and whatever the server actually sent as far as the program is concerned, which is where a large share of production TypeScript errors come from.

The tools for closing that gap are narrowing and validation. Type guards, discriminated unions and the unknown type let you start from I do not know what this is and prove what it is with code the compiler follows. Using unknown rather than any for external data is the single highest-value habit: any switches the checker off silently, unknown forces you to establish what you have before using it.

Beyond that, the type system is expressive enough to encode real rules: unions to make invalid states unrepresentable, generics to keep containers honest, mapped and conditional types to derive one shape from another so they cannot drift apart. It is also expressive enough to write types nobody can read, and the point at which a type needs a comment to explain it is usually the point to simplify it.

Check yourself

Two libraries define identically shaped interfaces with different names. What happens?

nothing left to check withvalidate here or not at allYour codechecked, structuralBuildtypes erasedRuntimeplain JavaScriptInternal valuesprotected by thecheckerParsed JSONwhatever the serversent
ServiceEdge / CDNData storeExternalDashed = asynchronousConsidered, not chosen
Types are compile time; the network is not
Evidence · 1 source
  1. 01
    TypeScript handbook: type compatibility

    That compatibility is structural rather than nominal, so identically shaped types from different libraries are interchangeable without any declared relationship.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

05

Modules, bundling and what you ship

intermediate

JavaScript spent a decade with no module system, then acquired two. CommonJS, with require and module.exports, resolves at run time and returns whatever the module currently exports. ES modules, with import and export, are static: the imports are known before any code runs, which is what allows a bundler to see the whole graph and remove what nothing uses.

That difference is the reason tree shaking works with one and not the other. A bundler can prove that an exported function is never imported and drop it, but only when the imports cannot change at run time. Importing a whole library for one function still costs the whole library if that library ships CommonJS, or if it has side effects at module scope that the bundler dare not remove.

What actually reaches a browser is a bundle, and its size is the number that matters. Compressed transfer size is the honest measure rather than the raw file size, and the thing to watch is not the total but what a first visit needs before the page is usable. Code splitting by route, and lazy loading anything below the first screen, is how a large application stays fast to start.

The dependency tree is where the weight hides. A single small utility can pull in a date library, a polyfill set and an internationalisation table, and none of it is visible in the import statement that caused it. Reading the bundle analysis once a quarter is a fifteen minute exercise that routinely finds a megabyte of something nobody meant to include.

Two smaller decisions carry more weight than they appear to. A dependency added for one helper function is a maintenance and supply chain cost that never goes away; writing the twelve lines yourself is often correct. And a polyfill for a feature every target browser has shipped for years is pure weight, which is what a browserslist configuration is for: it tells the toolchain what you actually support so it can stop compensating for browsers nobody runs.

Check yourself

Where does unexpected bundle weight usually come from?

provably unusedcannot prove anythingApplicationES module importknown before runningrequire()resolved at run timeBundlersees the whole graphOnly what is usedThe whole package
ClientServiceEdge / CDNData storeDashed = asynchronousConsidered, not chosen
Static imports can be pruned; dynamic ones cannot
Evidence · 1 source
  1. 01
    Node.js: packages and the exports field

    That a package declares its entry points through the standard exports map, which is what points a bundler at an ES build so that unused exports can be dropped.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.