{"post":{"slug":"promises-explained-2026","title":"Promises explained in 2026: states, chaining, and error handling","excerpt":"Promises replaced callback hell and underpin every async JavaScript API. Here is how they work, how to chain them correctly, and where they still trip people up.","category":"code","date":"March 19, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">P</span>romises were the answer to callback hell — deeply nested callbacks that made asynchronous JavaScript unreadable and error-prone. They shipped in ES6 (2015), became the foundation of `async/await` in ES2017, and are now the bedrock of every browser API and Node.js core module. A decade in, they are still misused in subtle ways that cause real production bugs.\n\n## What changed in 2026\n\n- **`Promise.withResolvers()` is now widely available.** Standardised in ES2024, this static method replaces the manual executor pattern for creating resolvable promises externally.\n- **Node 22+ makes unhandled rejections a hard crash by default.** The `--unhandled-rejections=throw` flag is now the default. Any unhandled rejection terminates the process.\n- **Temporal API ships in browsers.** `Temporal.Now.instant()` and date parsing return Promises or synchronous values with well-defined async wrappers — promise error handling applies everywhere.\n- **React 19 `use(promise)` hook.** A promise can now be passed directly to `use()` inside a component, with Suspense and error boundaries handling the states declaratively.\n\n## The three states\n\nA Promise is always in exactly one state:\n\n| State | Meaning | Can transition to |\n|---|---|---|\n| **Pending** | Operation in flight | Fulfilled or Rejected |\n| **Fulfilled** | Operation succeeded, value available | — (terminal) |\n| **Rejected** | Operation failed, reason available | — (terminal) |\n\nState transitions are permanent. A fulfilled promise cannot become rejected, and vice versa.\n\n```js\nconst p = new Promise((resolve, reject) => {\n  // Pending here\n  setTimeout(() => resolve(42), 1000);  // → Fulfilled with 42\n  // Or: reject(new Error(\"failed\"))   // → Rejected with Error\n});\n```\n\n## Chaining with `.then()` and `.catch()`\n\n`.then(onFulfilled, onRejected)` returns a **new** promise. This enables chaining without nesting.\n\n```js\nfetch('/api/user/1')\n  .then(res => res.json())           // parse JSON\n  .then(user => fetchOrders(user.id)) // fetch related data\n  .then(orders => console.log(orders))\n  .catch(err => console.error('Failed:', err));  // catches any rejection above\n```\n\nEach `.then()` transforms the value. A `.catch()` at the end catches any rejection anywhere in the chain.\n\n```js\n// Promise.withResolvers — ES2024\nconst { promise, resolve, reject } = Promise.withResolvers();\n// Resolve or reject externally\nsetTimeout(() => resolve('done'), 1000);\nawait promise;\n```\n\n## Concurrent execution patterns\n\n| Method | Behaviour | Use when |\n|---|---|---|\n| `Promise.all(arr)` | Fulfils when all fulfil; rejects on first rejection | All-or-nothing operations |\n| `Promise.allSettled(arr)` | Always fulfils with array of outcomes | Need all results regardless of failures |\n| `Promise.race(arr)` | Settles with the first settled promise | Timeout patterns, fastest-wins |\n| `Promise.any(arr)` | Fulfils with first fulfilled; rejects if all reject | First-success-wins (fallback chains) |\n\n```js\n// Run three requests in parallel; fail if any fails\nconst [user, orders, prefs] = await Promise.all([\n  fetchUser(id),\n  fetchOrders(id),\n  fetchPrefs(id),\n]);\n\n// Run three requests; collect all outcomes\nconst results = await Promise.allSettled([\n  fetchUser(id),\n  fetchOrders(id),\n  fetchPrefs(id),\n]);\nresults.forEach(r => {\n  if (r.status === 'fulfilled') console.log(r.value);\n  else console.error(r.reason);\n});\n```\n\n## Relationship to async/await\n\n`async/await` is syntactic sugar over Promises. An `async` function always returns a Promise; `await` pauses execution until the awaited Promise settles.\n\n```js\n// Equivalent: .then() chain\nfunction loadUser(id) {\n  return fetch(`/api/users/${id}`)\n    .then(res => res.json())\n    .catch(err => { throw new Error(`User load failed: ${err.message}`); });\n}\n\n// Equivalent: async/await\nasync function loadUser(id) {\n  try {\n    const res = await fetch(`/api/users/${id}`);\n    return await res.json();\n  } catch (err) {\n    throw new Error(`User load failed: ${err.message}`);\n  }\n}\n```\n\nBoth produce the same promise chain under the hood. `async/await` reads more linearly, especially with error handling.\n\n## Common mistakes\n\n**Forgetting to return the Promise inside `.then()`.**\n\n```js\n// Bug: the inner fetch is not returned; chain does not wait for it\n.then(user => { fetch(`/api/orders/${user.id}`); })  // returns undefined\n\n// Fix: return the promise\n.then(user => fetch(`/api/orders/${user.id}`))\n```\n\n**Not handling rejections.**\n\n```js\n// Node 22+: this crashes the process\nsomePromise.then(doSomething);  // no .catch()\n\n// Fix:\nsomePromise.then(doSomething).catch(handleError);\n// Or use async/await with try/catch\n```\n\n**`await` inside a `.forEach()` loop.** `Array.forEach` does not await async callbacks — the loop completes immediately.\n\n```js\n// Bug: all requests fire simultaneously, errors are swallowed\nitems.forEach(async (item) => { await process(item); });\n\n// Fix: use for...of\nfor (const item of items) { await process(item); }\n// Or, for parallel: await Promise.all(items.map(process));\n```\n\n**Mixing `.then()` chains and `await` in the same function.** It works but confuses readers. Pick one style.\n\n## Error handling comparison\n\n| Pattern | Catches synchronous errors | Catches async rejections | Readable |\n|---|---|---|---|\n| `.then().catch()` | No (need try inside .then) | Yes | Medium |\n| `async/await` + `try/catch` | Yes | Yes | High |\n| Global `unhandledRejection` handler | Yes (fallback only) | Yes (fallback only) | N/A — last resort |\n\n## How to pick\n\n1. **Need to run multiple promises in parallel?** → `Promise.all` or `Promise.allSettled`.\n2. **Timeout pattern?** → `Promise.race([operation, timeout])`.\n3. **Sequential async steps with error handling?** → `async/await` + `try/catch`.\n4. **Streaming chain transformations?** → `.then()` chains work well for pipeline-style code.\n5. **Exposing a promise to external code for manual resolution?** → `Promise.withResolvers()`.\n\n## What to skip\n\n- **Callback-based wrappers when Promise APIs exist** — `util.promisify` is there if you need it; prefer the native promise API.\n- **`async` functions that never `await` anything** — they add wrapping overhead for no benefit; return a plain value or a plain promise.\n- **Deeply nested `.then()` chains** — if you find yourself indenting `.then()` inside `.then()`, use `async/await`. See [Async/await explained in 2026](/blog/async-await-explained-2026) for the full treatment.\n\n## FAQ\n\n**Can a Promise be cancelled?**\nNo — the native Promise API has no cancellation. Use `AbortController` with `fetch` and other supporting APIs, or the `AbortSignal` pattern for custom async operations.\n\n**What happens if both `resolve` and `reject` are called?**\nOnly the first call takes effect. After the first transition, subsequent calls to `resolve` or `reject` are silently ignored.\n\n**Is `Promise.resolve(value)` the same as `new Promise(res => res(value))`?**\nFunctionally yes, but `Promise.resolve` is more efficient and handles the case where `value` is itself a promise (it returns it directly instead of wrapping it).\n\n**Why does `.catch(fn)` work the same as `.then(undefined, fn)`?**\nBecause `.catch(fn)` is literally defined as `.then(undefined, fn)`. The only functional difference is that `.then(onFulfilled, onRejected)` does not catch errors thrown by `onFulfilled` in the same call, while chained `.then().catch()` does.\n\n## Where to go next\n\n- [Async/await explained in 2026](/blog/async-await-explained-2026)\n- [Closures explained in 2026](/blog/closures-explained-2026)\n- [Error handling explained in 2026](/blog/error-handling-explained-2026)","hue":318,"metaTitle":"Promises Explained 2026: States, Chaining, Error Handling","metaDescription":"Thorough 2026 guide to JavaScript Promises: the three states, chaining, Promise.all vs allSettled, common mistakes, and async/await relationship.","keywords":["promises explained 2026","javascript promises guide","promise states pending fulfilled rejected","promise chaining 2026","promise all vs allsettled","unhandled promise rejection","async javascript 2026","promise error handling","promise vs async await","promises interview 2026"],"tldr":["**A Promise is a proxy for a value not yet available** — it is in one of three states: pending, fulfilled, or rejected, and can only transition once.","**`.then()` and `.catch()` return new Promises** — enabling clean chaining without nested callbacks.","**Unhandled rejections crash Node 18+ and log warnings in browsers** — always attach a `.catch()` or `try/catch` around `await`.","**`Promise.all` fails fast; `Promise.allSettled` does not** — pick based on whether one failure should cancel the whole batch.","**Skip:** mixing `.then()` chains and `await` in the same flow — pick one style per function; mixing them produces confusing control flow."],"readTime":"5 min read","wordCount":1050},"related":[{"slug":"what-is-a-promise-in-javascript-2026","title":"What is a Promise in JavaScript in 2026? Async results explained","excerpt":"A Promise is a JavaScript object that represents a value that is not ready yet, like a network response. This guide explains promise states, then and catch, and how async await fits in.","category":"code","date":"May 25, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":240},{"slug":"what-is-a-promise-chain-2026","title":"What Is a Promise Chain in 2026? Chaining .then Explained","excerpt":"A promise chain links multiple then calls so each step waits for the last one to finish. Here is how chaining actually works, where it breaks, and how it compares to async await in 2026.","category":"code","date":"July 20, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320},{"slug":"async-await-explained-2026","title":"Async/await explained in 2026: how it works and when to use it","excerpt":"Async/await is the dominant concurrency model in modern JavaScript, Python, and Rust — but most developers still misuse it. Here is the definitive 2026 guide.","category":"code","date":"February 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":319}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"postgres-vs-sqlite-2026","title":"Postgres vs SQLite in 2026: the honest comparison for real projects","excerpt":"SQLite is no longer just for embedded devices — it runs production web apps at scale. Here is when to choose it over Postgres in 2026.","category":"code","date":"March 19, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":312},"older":{"slug":"prompt-caching-explained-2026","title":"Prompt caching explained in 2026: cut your LLM costs by up to 90%","excerpt":"Prompt caching lets you reuse the expensive KV-computed portion of a prompt across calls. In 2026 it is the single highest-leverage cost optimization for most LLM applications.","category":"ai","date":"March 19, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":210}}