[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-unhandledrejection-swallowed-promise-errors":44,"search-suggestions":235,"related-unhandledrejection-swallowed-promise-errors":276,"code:js:true:mf0n2v":409,"code:js:true:1qok1pp":410,"comments-01a1017f-c2ac-70af-a013-25321448f53a":411},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":53,"likeCount":19,"commentCount":19,"author":54,"vertical":59,"topic":60,"tags":63,"_count":75,"playground":77,"body":79,"bodyMd":218,"seo":219,"translationGroupId":222,"series":48,"podcastUrl":48,"verticalId":5,"thread":223,"assessments":225,"translations":231,"quiz":233},"01a1017f-c2ac-70af-a013-25321448f53a","unhandledrejection-swallowed-promise-errors","Stop Losing Promise Errors. `unhandledrejection` Catches Them.",null,"A rejected promise with no .catch doesn't crash and doesn't show up in your error tracker — it just disappears. The unhandledrejection event is the global safety net most apps never wire up.","\u002Fmedia\u002Fcovers\u002Funhandledrejection-swallowed-promise-errors.png",5,"2026-10-08T06:10:23.452Z",51,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},"javascript","JavaScript",[64,66,69,72],{"slug":61,"name":65,"color":48},"Javascript",{"slug":67,"name":68,"color":48},"webdev","Webdev",{"slug":70,"name":71,"color":48},"programming","Programming",{"slug":73,"name":74,"color":48},"debugging","Debugging",{"assessments":76},1,{"slug":46,"title":78},"unhandledrejection — interactive playground",{"blocks":80,"version":76},[81,85,88,92,95,101,104,107,111,114,120,123,126,129,133,136,139,142,145,148,151,154,158,161,164,167,170,173,176,179,182,185,188,191,194,200,203,206,209,212],{"id":82,"html":83,"type":84},"b1","\u003Cp>Three weeks. That&#39;s how long it took anyone to notice that a subset of checkouts were quietly failing — not erroring, not crashing, just... not completing. No red line in the dashboard. No spike in the error tracker. Support tickets trickled in (&quot;I clicked pay and nothing happened&quot;), got marked as user error, and closed.\u003C\u002Fp>","paragraph",{"id":86,"html":87,"type":84},"b2","\u003Cp>The bug was two lines of code: a \u003Ccode>fetch()\u003C\u002Fcode> call inside an async handler, with no \u003Ccode>.catch()\u003C\u002Fcode> anywhere on its chain. When the payment API returned a validation error, the promise rejected, the handler silently stopped executing, and the rejection went — nowhere. Not to the console in a way anyone was watching. Not to Sentry. Not to anyone.\u003C\u002Fp>",{"id":89,"html":90,"text":90,"type":91,"level":31},"b3","The code that looks finished","heading",{"id":93,"html":94,"type":84},"b4","\u003Cp>This is the version almost everyone ships, because it looks complete:\u003C\u002Fp>",{"id":96,"code":97,"type":98,"language":99,"highlight":100},"b5","button.addEventListener(\"click\", async () => {\n  button.disabled = true;\n  const res = await fetch(\"\u002Fapi\u002Fcheckout\", { method: \"POST\", body: cartPayload });\n  const order = await res.json();\n  showConfirmation(order);\n});","code","js",[],{"id":102,"html":103,"type":84},"b6","\u003Cp>It handles the happy path. It \u003Ccode>await\u003C\u002Fcode>s both calls. There&#39;s no obvious hole. Run it against a working API and it&#39;s correct.\u003C\u002Fp>",{"id":105,"html":106,"type":84},"b7","\u003Cp>Now make \u003Ccode>fetch\u003C\u002Fcode> reject — a network drop, a CORS misconfiguration, or \u003Ccode>res.json()\u003C\u002Fcode> throwing because the server returned an HTML error page instead of JSON. The \u003Ccode>await\u003C\u002Fcode> re-throws inside the async function, the function&#39;s returned promise rejects, and since nothing ever attaches a \u003Ccode>.catch()\u003C\u002Fcode> to \u003Cem>that\u003C\u002Fem> promise — the event listener doesn&#39;t return it to anyone, it just fires it and walks away — the rejection has no handler. \u003Ccode>button.disabled\u003C\u002Fcode> stays \u003Ccode>true\u003C\u002Fcode> forever. The user sees a button that quietly stopped working. And unless you know to look, you&#39;ll never know it happened.\u003C\u002Fp>",{"id":108,"html":109,"text":110,"type":91,"level":31},"b8","Why your error tracker didn&#39;t catch it","Why your error tracker didn't catch it",{"id":112,"html":113,"type":84},"b9","\u003Cp>This is the part that surprises people: an unhandled promise rejection is \u003Cstrong>not\u003C\u002Fstrong> the same event as a runtime error, and plenty of error-monitoring setups only wire up the one they thought of first.\u003C\u002Fp>",{"id":115,"type":116,"items":117,"ordered":18},"b10","list",[118,119],"A thrown error outside a promise fires \u003Ccode>window.onerror\u003C\u002Fcode> (or the \u003Ccode>error\u003C\u002Fcode> event on \u003Ccode>window\u003C\u002Fcode>) — most basic error tracking hooks this.","A promise that rejects with nobody attached to catch it fires a \u003Cem>different\u003C\u002Fem> event: \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002Funhandledrejection_event\">\u003Ccode>unhandledrejection\u003C\u002Fcode>\u003C\u002Fa>. If you never listen for it, the only sign it happened is a console line — \u003Ccode>Uncaught (in promise) TypeError: ...\u003C\u002Fcode> — that nobody sees unless DevTools is already open and someone is looking at exactly the right tab.",{"id":121,"html":122,"type":84},"b11","\u003Cp>Full error-tracking SDKs like Sentry do wire this up for you by default. A hand-rolled \u003Ccode>window.onerror = reportError\u003C\u002Fcode> does not. That gap is exactly where the checkout bug lived for three weeks.\u003C\u002Fp>",{"id":124,"html":125,"text":125,"type":91,"level":31},"b12","The listener that closes the gap",{"id":127,"html":128,"type":84},"b13","\u003Cp>The fix is one global listener, and it&#39;s been supported in every major browser for years:\u003C\u002Fp>",{"id":130,"code":131,"type":98,"language":99,"highlight":132},"b14","window.addEventListener(\"unhandledrejection\", (event) => {\n  \u002F\u002F event.reason is whatever the promise rejected with —\n  \u002F\u002F usually an Error, but not guaranteed (someone could `reject(\"oops\")`)\n  reportError(event.reason, { source: \"unhandledrejection\" });\n\n  \u002F\u002F optional: stop the \"Uncaught (in promise) ...\" console noise,\n  \u002F\u002F now that you've reported it somewhere that actually gets read\n  event.preventDefault();\n});",[],{"id":134,"html":135,"type":84},"b15","\u003Cp>\u003Ccode>event\u003C\u002Fcode> here is a \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FPromiseRejectionEvent\">\u003Ccode>PromiseRejectionEvent\u003C\u002Fcode>\u003C\u002Fa>, with two properties worth knowing: \u003Ccode>event.promise\u003C\u002Fcode> (the promise that rejected) and \u003Ccode>event.reason\u003C\u002Fcode> (the value it rejected with — often an \u003Ccode>Error\u003C\u002Fcode>, but JavaScript lets you reject with anything, so don&#39;t assume it has a \u003Ccode>.message\u003C\u002Fcode>). Calling \u003Ccode>event.preventDefault()\u003C\u002Fcode> suppresses the browser&#39;s own &quot;Uncaught (in promise)&quot; console message — worth doing once you&#39;re actually reporting the rejection somewhere, so you&#39;re not logging it twice.\u003C\u002Fp>",{"id":137,"html":138,"type":84},"b16","\u003Cp>There&#39;s a companion event, \u003Ca href=\"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002Frejectionhandled_event\">\u003Ccode>rejectionhandled\u003C\u002Fcode>\u003C\u002Fa>, that fires if a \u003Ccode>.catch()\u003C\u002Fcode> shows up \u003Cem>after\u003C\u002Fem> the fact — say, a \u003Ccode>.then()\u003C\u002Fcode> chained in asynchronously once some other code finishes loading. It exists so you can retract a report you already sent for a rejection that turned out to be handled, just late. Most apps never need it; it&#39;s there if your reporting pipeline needs to avoid false positives on timing-sensitive code.\u003C\u002Fp>",{"id":140,"html":141,"type":84},"b17","\u003C!-- playground:start -->",{"id":143,"html":144,"text":144,"type":91,"level":31},"b18","🎮 Try it yourself",{"id":146,"html":147,"type":84},"b19","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Funhandledrejection-swallowed-promise-errors\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":149,"html":150,"type":84},"b20","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":152,"html":153,"type":84},"b21","\u003C!-- playground:end -->",{"id":155,"html":156,"text":157,"type":91,"level":31},"b22","The one thing it doesn&#39;t do","The one thing it doesn't do",{"id":159,"html":160,"type":84},"b23","\u003Cp>Be clear about what this buys you, because it&#39;s tempting to treat a global listener like a fix: \u003Cstrong>it isn&#39;t one.\u003C\u002Fstrong> \u003Ccode>unhandledrejection\u003C\u002Fcode> doesn&#39;t stop the checkout button from getting stuck, doesn&#39;t retry the request, and doesn&#39;t tell the user anything went wrong. All it does is make a failure that was previously invisible show up somewhere you&#39;ll actually read it. The underlying bug — a \u003Ccode>.catch()\u003C\u002Fcode> or a \u003Ccode>try\u003C\u002Fcode>\u002F\u003Ccode>catch\u003C\u002Fcode> that should have been there — still needs fixing at the source. What the listener changes is how long that bug survives before someone notices.\u003C\u002Fp>",{"id":162,"html":163,"type":84},"b24","\u003Cp>It&#39;s also browser-only as written above: in Node.js, the equivalent is \u003Ccode>process.on(&quot;unhandledRejection&quot;, (reason, promise) =&gt; { ... })\u003C\u002Fcode>, and every Node release since v15 (2020) goes further than a warning — by default, an unhandled rejection now terminates the process instead of just logging it, which is its own argument for not leaving async error handling to chance.\u003C\u002Fp>",{"id":165,"html":166,"text":166,"type":91,"level":31},"b25","What this is really for",{"id":168,"html":169,"type":84},"b26","\u003Cp>Treat \u003Ccode>unhandledrejection\u003C\u002Fcode> the way you&#39;d treat a smoke detector: it doesn&#39;t put out the fire, it just guarantees you find out about it before the room fills with smoke. Wire it into whatever already collects your errors — Sentry, a logging endpoint, even just a styled console group in staging — and the next silent failure stops being silent. The checkout bug above wasn&#39;t hard to fix once someone saw it. The three weeks were the cost of nobody seeing it.\u003C\u002Fp>",{"id":171,"html":172,"type":84},"b27","\u003Cp>Go check right now: does your app have a global \u003Ccode>unhandledrejection\u003C\u002Fcode> listener, or does it only catch the errors you remembered to wrap in \u003Ccode>try\u003C\u002Fcode>\u002F\u003Ccode>catch\u003C\u002Fcode>?\u003C\u002Fp>",{"id":174,"html":175,"type":84},"b28","\u003C!-- quiz:start -->",{"id":177,"html":178,"text":178,"type":91,"level":31},"b29","🧠 Test yourself",{"id":180,"html":181,"type":84},"b30","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Funhandledrejection-swallowed-promise-errors\u002Fquiz\">Take the 8-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":183,"html":184,"type":84},"b31","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":186,"html":187,"type":84},"b32","\u003C!-- quiz:end -->",{"id":189,"html":190,"type":84},"b33","\u003C!-- related:start -->",{"id":192,"html":193,"text":193,"type":91,"level":31},"b34","📚 Read next",{"id":195,"type":116,"items":196,"ordered":18},"b35",[197,198,199],"\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ferror-cause-rethrow-context\">You&#39;re rethrowing errors and losing context. \u003Ccode>Error.cause\u003C\u002Fcode> fixes that.\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-try-synchronous-throw-safety\">My .catch() Never Ran. The Bug Was Two Lines Above It\u003C\u002Fa>","\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-concurrency-all-settled-any-race\">You reach for \u003Ccode>Promise.all\u003C\u002Fcode> for every concurrent request. Here&#39;s when to use the other three.\u003C\u002Fa>",{"id":201,"html":202,"type":84},"b36","\u003C!-- related:end -->",{"id":204,"type":205},"b37","divider",{"id":207,"html":208,"type":84},"b38","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":210,"html":211,"type":84},"b39","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":213,"type":116,"items":214,"ordered":18},"b40",[215,216,217],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Three weeks. That's how long it took anyone to notice that a subset of checkouts were quietly failing — not erroring, not crashing, just... not completing. No red line in the dashboard. No spike in the error tracker. Support tickets trickled in (\"I clicked pay and nothing happened\"), got marked as user error, and closed.\n\nThe bug was two lines of code: a `fetch()` call inside an async handler, with no `.catch()` anywhere on its chain. When the payment API returned a validation error, the promise rejected, the handler silently stopped executing, and the rejection went — nowhere. Not to the console in a way anyone was watching. Not to Sentry. Not to anyone.\n\n## The code that looks finished\n\nThis is the version almost everyone ships, because it looks complete:\n\n```js\nbutton.addEventListener(\"click\", async () => {\n  button.disabled = true;\n  const res = await fetch(\"\u002Fapi\u002Fcheckout\", { method: \"POST\", body: cartPayload });\n  const order = await res.json();\n  showConfirmation(order);\n});\n```\n\nIt handles the happy path. It `await`s both calls. There's no obvious hole. Run it against a working API and it's correct.\n\nNow make `fetch` reject — a network drop, a CORS misconfiguration, or `res.json()` throwing because the server returned an HTML error page instead of JSON. The `await` re-throws inside the async function, the function's returned promise rejects, and since nothing ever attaches a `.catch()` to *that* promise — the event listener doesn't return it to anyone, it just fires it and walks away — the rejection has no handler. `button.disabled` stays `true` forever. The user sees a button that quietly stopped working. And unless you know to look, you'll never know it happened.\n\n## Why your error tracker didn't catch it\n\nThis is the part that surprises people: an unhandled promise rejection is **not** the same event as a runtime error, and plenty of error-monitoring setups only wire up the one they thought of first.\n\n- A thrown error outside a promise fires `window.onerror` (or the `error` event on `window`) — most basic error tracking hooks this.\n- A promise that rejects with nobody attached to catch it fires a *different* event: [`unhandledrejection`](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002Funhandledrejection_event). If you never listen for it, the only sign it happened is a console line — `Uncaught (in promise) TypeError: ...` — that nobody sees unless DevTools is already open and someone is looking at exactly the right tab.\n\nFull error-tracking SDKs like Sentry do wire this up for you by default. A hand-rolled `window.onerror = reportError` does not. That gap is exactly where the checkout bug lived for three weeks.\n\n## The listener that closes the gap\n\nThe fix is one global listener, and it's been supported in every major browser for years:\n\n```js\nwindow.addEventListener(\"unhandledrejection\", (event) => {\n  \u002F\u002F event.reason is whatever the promise rejected with —\n  \u002F\u002F usually an Error, but not guaranteed (someone could `reject(\"oops\")`)\n  reportError(event.reason, { source: \"unhandledrejection\" });\n\n  \u002F\u002F optional: stop the \"Uncaught (in promise) ...\" console noise,\n  \u002F\u002F now that you've reported it somewhere that actually gets read\n  event.preventDefault();\n});\n```\n\n`event` here is a [`PromiseRejectionEvent`](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FPromiseRejectionEvent), with two properties worth knowing: `event.promise` (the promise that rejected) and `event.reason` (the value it rejected with — often an `Error`, but JavaScript lets you reject with anything, so don't assume it has a `.message`). Calling `event.preventDefault()` suppresses the browser's own \"Uncaught (in promise)\" console message — worth doing once you're actually reporting the rejection somewhere, so you're not logging it twice.\n\nThere's a companion event, [`rejectionhandled`](https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWindow\u002Frejectionhandled_event), that fires if a `.catch()` shows up *after* the fact — say, a `.then()` chained in asynchronously once some other code finishes loading. It exists so you can retract a report you already sent for a rejection that turned out to be handled, just late. Most apps never need it; it's there if your reporting pipeline needs to avoid false positives on timing-sensitive code.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Funhandledrejection-swallowed-promise-errors\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## The one thing it doesn't do\n\nBe clear about what this buys you, because it's tempting to treat a global listener like a fix: **it isn't one.** `unhandledrejection` doesn't stop the checkout button from getting stuck, doesn't retry the request, and doesn't tell the user anything went wrong. All it does is make a failure that was previously invisible show up somewhere you'll actually read it. The underlying bug — a `.catch()` or a `try`\u002F`catch` that should have been there — still needs fixing at the source. What the listener changes is how long that bug survives before someone notices.\n\nIt's also browser-only as written above: in Node.js, the equivalent is `process.on(\"unhandledRejection\", (reason, promise) => { ... })`, and every Node release since v15 (2020) goes further than a warning — by default, an unhandled rejection now terminates the process instead of just logging it, which is its own argument for not leaving async error handling to chance.\n\n## What this is really for\n\nTreat `unhandledrejection` the way you'd treat a smoke detector: it doesn't put out the fire, it just guarantees you find out about it before the room fills with smoke. Wire it into whatever already collects your errors — Sentry, a logging endpoint, even just a styled console group in staging — and the next silent failure stops being silent. The checkout bug above wasn't hard to fix once someone saw it. The three weeks were the cost of nobody seeing it.\n\nGo check right now: does your app have a global `unhandledrejection` listener, or does it only catch the errors you remembered to wrap in `try`\u002F`catch`?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 8-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Funhandledrejection-swallowed-promise-errors\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n\u003C!-- related:start -->\n\n## 📚 Read next\n\n- [You're rethrowing errors and losing context. `Error.cause` fixes that.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ferror-cause-rethrow-context)\n- [My .catch() Never Ran. The Bug Was Two Lines Above It](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-try-synchronous-throw-safety)\n- [You reach for `Promise.all` for every concurrent request. Here's when to use the other three.](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fpromise-concurrency-all-settled-any-race)\n\n\u003C!-- related:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":47,"canonical":220,"description":221},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Funhandledrejection-swallowed-promise-errors","A rejected promise with no .catch doesn't crash and doesn't show up in your error tracker — it just disappears. The unhandledrejection event is the global safety net most apps neve","01a1017f-c2ac-70af-a013-28358bc9bac2",{"id":224,"locked":18},"01a1017f-c2e5-738a-bdcb-3d39f53d781f",[226],{"id":227,"slug":46,"title":228,"_count":229},"01a1017f-c2ff-719c-bb28-dfaa8e16dd12","unhandledrejection: catching swallowed promise errors",{"questions":230},8,[232],{"locale":13,"slug":46},{"id":227,"slug":46,"title":228,"_count":234,"questionCount":230},{"questions":230},[236,238,240,244,248,252,256,260,264,268,271,275],{"slug":67,"name":68,"articles":237},128,{"slug":61,"name":65,"articles":239},106,{"slug":241,"name":242,"articles":243},"frontend","Frontend",80,{"slug":245,"name":246,"articles":247},"tutorial","Tutorial",49,{"slug":249,"name":250,"articles":251},"css","Css",40,{"slug":253,"name":254,"articles":255},"typescript","Typescript",18,{"slug":257,"name":258,"articles":259},"performance","Performance",16,{"slug":261,"name":262,"articles":263},"react","React",15,{"slug":265,"name":266,"articles":267},"browser","Browser",12,{"slug":269,"name":270,"articles":267},"node","Node",{"slug":272,"name":273,"articles":274},"html","Html",9,{"slug":70,"name":71,"articles":230},{"items":277,"meta":407},[278,290,309,327,348,366,389],{"id":45,"slug":46,"title":47,"subtitle":48,"excerpt":49,"coverUrl":50,"locale":13,"readingMinutes":51,"publishedAt":52,"viewCount":279,"likeCount":19,"commentCount":19,"author":280,"vertical":281,"topic":282,"tags":283,"_count":288,"playground":289,"hasQuiz":17,"hasPlayground":17},52,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[284,285,286,287],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":73,"name":74,"color":48},{"assessments":76},{"slug":46},{"id":291,"slug":292,"title":293,"subtitle":48,"excerpt":294,"coverUrl":295,"locale":13,"readingMinutes":296,"publishedAt":297,"viewCount":298,"likeCount":19,"commentCount":19,"author":299,"vertical":300,"topic":301,"tags":302,"_count":307,"playground":308,"hasQuiz":17,"hasPlayground":17},"01a0fb3c-4e87-7604-9b6e-d9f518d232be","fetch-retry-exponential-backoff","Stop Retrying Immediately. Exponential Backoff Fixes It.","Retrying a failed fetch() right away feels responsible — until the server that failed was already overloaded, and five instant retries per client turn a blip into an outage. Exponential backoff with jitter is the fix.","\u002Fmedia\u002Fcovers\u002Ffetch-retry-exponential-backoff.png",6,"2026-10-07T06:10:20.335Z",105,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[303,304,305,306],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":70,"name":71,"color":48},{"slug":269,"name":270,"color":48},{"assessments":76},{"slug":292},{"id":310,"slug":311,"title":312,"subtitle":48,"excerpt":313,"coverUrl":314,"locale":13,"readingMinutes":296,"publishedAt":315,"viewCount":316,"likeCount":19,"commentCount":19,"author":317,"vertical":318,"topic":319,"tags":320,"_count":325,"playground":326,"hasQuiz":17,"hasPlayground":17},"01a0f615-e1c2-729c-b09a-6e35ad0eba80","storage-event-cross-tab-sync","I Built Cross-Tab Logout. The storage Event Skipped One Tab.","A cross-tab 'log out everywhere' feature built on the storage event worked in every tab except the one the user actually clicked in. The storage event never fires in the tab that made the change — and that's spec, not a bug.","\u002Fmedia\u002Fcovers\u002Fstorage-event-cross-tab-sync.png","2026-10-06T06:11:02.312Z",164,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[321,322,323,324],{"slug":61,"name":65,"color":48},{"slug":67,"name":68,"color":48},{"slug":241,"name":242,"color":48},{"slug":265,"name":266,"color":48},{"assessments":76},{"slug":311},{"id":328,"slug":329,"title":330,"subtitle":48,"excerpt":331,"coverUrl":332,"locale":13,"readingMinutes":333,"publishedAt":334,"viewCount":335,"likeCount":19,"commentCount":19,"author":336,"vertical":337,"topic":338,"tags":341,"_count":346,"playground":347,"hasQuiz":17,"hasPlayground":17},"01a0af18-2d15-7347-b2f4-6cdfa2ce56fa","vue-weekly-definemodel-v-model-contract","Vue defineModel: The v-model Contract, Explained","Vue's defineModel macro replaces the modelValue prop and update:modelValue event with one ref. The exact v-model contract behind it, verified against Vue 3.5.","\u002Fmedia\u002Fcovers\u002Fvue-weekly-definemodel-v-model-contract.png",13,"2026-10-05T06:11:02.623Z",266,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":339,"name":340},"vue","Vue",[342,343,344,345],{"slug":339,"name":340,"color":48},{"slug":61,"name":65,"color":48},{"slug":245,"name":246,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":329},{"id":349,"slug":350,"title":351,"subtitle":48,"excerpt":352,"coverUrl":353,"locale":13,"readingMinutes":296,"publishedAt":354,"viewCount":355,"likeCount":19,"commentCount":19,"author":356,"vertical":357,"topic":358,"tags":359,"_count":364,"playground":365,"hasQuiz":17,"hasPlayground":17},"01a0f0ef-d9b8-719b-97a3-fb43e840c4d1","bfcache-beforeunload-back-button","`beforeunload` Can Still Kill Your bfcache. Mount It Only When Dirty.","A `beforeunload` listener you left mounted forever can still quietly cost you bfcache eligibility — and an `unload` listener reliably will. Here's the pagehide\u002Fpageshow fix, and why 'mount it only when there's something to lose' is the durable rule.","\u002Fmedia\u002Fcovers\u002Fbfcache-beforeunload-back-button.png","2026-10-05T06:10:30.618Z",181,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":61,"name":62},[360,361,362,363],{"slug":67,"name":68,"color":48},{"slug":61,"name":65,"color":48},{"slug":257,"name":258,"color":48},{"slug":241,"name":242,"color":48},{"assessments":76},{"slug":350},{"id":367,"slug":368,"title":369,"subtitle":48,"excerpt":370,"coverUrl":371,"locale":13,"readingMinutes":372,"publishedAt":373,"viewCount":374,"likeCount":19,"commentCount":19,"author":375,"vertical":376,"topic":377,"tags":380,"_count":387,"playground":388,"hasQuiz":17,"hasPlayground":17},"01a0f8de-661d-724c-a8c3-3aef432860c4","nuxt-weekly-ssr-streaming-route-rules","Nuxt 4.5 SSR Streaming: The Route Rules That Disable It","Nuxt 4.5's experimental SSR streaming flushes HTML early, but cache, isr, swr, redirect, and ssr:false route rules silently fall back to buffering. Cheat sheet included.","\u002Fmedia\u002Fcovers\u002Fnuxt-weekly-ssr-streaming-route-rules.png",14,"2026-10-04T12:03:48.007Z",256,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":378,"name":379},"nuxt","Nuxt",[381,382,385,386],{"slug":378,"name":379,"color":48},{"slug":383,"name":384,"color":48},"ssr","SSR",{"slug":61,"name":65,"color":48},{"slug":245,"name":246,"color":48},{"assessments":76},{"slug":368},{"id":390,"slug":391,"title":392,"subtitle":48,"excerpt":393,"coverUrl":394,"locale":13,"readingMinutes":263,"publishedAt":395,"viewCount":396,"likeCount":19,"commentCount":19,"author":397,"vertical":398,"topic":399,"tags":400,"_count":405,"playground":406,"hasQuiz":17,"hasPlayground":17},"01a0f615-d2be-750b-839e-02fd75291b4e","react-weekly-viewtransition-component","React 19.3 ViewTransition: Animate State Without Losing It","React 19.3's ViewTransition component animates state changes, not just page swaps — the mental model, enter\u002Fexit\u002Fupdate\u002Fshare, and a cheat sheet.","\u002Fmedia\u002Fcovers\u002Freact-weekly-viewtransition-component.png","2026-10-03T11:22:26.437Z",299,{"id":55,"name":56,"username":57,"avatarUrl":48,"headline":58},{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":261,"name":262},[401,402,403,404],{"slug":261,"name":262,"color":48},{"slug":61,"name":65,"color":48},{"slug":245,"name":246,"color":48},{"slug":67,"name":68,"color":48},{"assessments":76},{"slug":391},{"page":76,"perPage":408,"total":239,"totalPages":259},7,"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">button\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"click\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">async\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  button\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>disabled \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-bool\">true\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> res \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> \u003Cspan class=\"shj-func\">fetch\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"\u002Fapi\u002Fcheckout\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> method\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"POST\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> body\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> cartPayload \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> order \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">await\u003C\u002Fspan> res\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">json\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-func\">showConfirmation\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>order\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">\"unhandledrejection\"\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F event.reason is whatever the promise rejected with —\n\u003C\u002Fspan>  \u003Cspan class=\"shj-cmnt\">\u002F\u002F usually an Error, but not guaranteed (someone could `reject(\"oops\")`)\n\u003C\u002Fspan>  \u003Cspan class=\"shj-func\">reportError\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>reason\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> source\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">\"unhandledrejection\"\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-cmnt\">\u002F\u002F optional: stop the \"Uncaught (in promise) ...\" console noise,\n\u003C\u002Fspan>  \u003Cspan class=\"shj-cmnt\">\u002F\u002F now that you've reported it somewhere that actually gets read\n\u003C\u002Fspan>  event\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">preventDefault\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>",{"locked":18,"total":19,"comments":412},[]]