Promises
A Promise is an object representing the eventual completion or rejection of an asynchronous operation. It has three states: pending, fulfilled, or rejected. Callbacks registered with .then() and .catch() are scheduled as microtasks.
A Promise is an IOU note from a restaurant kitchen. Pending = order placed. Fulfilled = food arrives. Rejected = 'sorry, we are out of that.' You can chain IOUs: 'when my food arrives, THEN bring dessert.'
Promises are eager — the executor function runs synchronously when the Promise is created. .then() always returns a new Promise, enabling chaining. Returning a value from .then() wraps it in a resolved Promise; throwing unwraps to a rejected Promise. .catch(fn) is shorthand for .then(undefined, fn). Unhandled promise rejections trigger a global event and in Node.js can crash the process (--unhandled-rejections=throw is the default since Node 15).
Promise resolution is defined by the Promise Resolution Procedure (spec §25.6.1.3.2): if the resolution value is a thenable (has a .then method), the Promise 'assimilates' it by calling .then — this is why custom thenables work with native Promises. Promise chaining always inserts at least one microtask hop, even for already-resolved Promises — this ensures consistent async behavior and prevents stack overflows. The Zalgo problem (sometimes sync, sometimes async callbacks) is solved by Promises: .then is always async. Promise.resolve(x) where x is already a native Promise returns x itself (same reference) without wrapping — an optimization in the spec.
Promises provide a composable, chainable way to handle asynchronous results. The executor runs synchronously; .then callbacks always run as microtasks. Chaining works because .then returns a new Promise — throw inside .then produces a rejection, return produces a resolution. The most important thing to know in interviews: Promise chains only catch errors if you have a .catch at the end or a second argument to .then — silently swallowed rejections are a common production bug.
Wrapping an existing Promise in a new Promise constructor (the 'deferred anti-pattern') is unnecessary and breaks error propagation: new Promise((resolve) => resolve(existingPromise)) — the outer promise will resolve with the inner promise as its value if done incorrectly. Use Promise.resolve(existingPromise) instead, which returns the existing promise directly.