[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-error-cause-rethrow-context":32,"quiz-article-error-cause-rethrow-context":48},[4,20],{"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","Build. Learn. Ship.","Practical courses, engineering-grade articles and open-source tools for people who ship.","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.","Structured paths, listening drills and spaced repetition that actually sticks.","amber-400","⌘",[13,15,29],"es",{"courses":17,"paths":17,"articles":18,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":17},2,{"id":33,"slug":34,"kind":35,"title":36,"description":37,"config":38,"verticalId":5,"vertical":43,"course":40,"_count":44,"access":45,"attempts":47,"questionCount":39},"019fe776-680d-7398-9cd7-ac0ca0d6511e","error-cause-rethrow-context","PRACTICE_QUIZ","Error.cause: preserving context on rethrow","The cause option lets a rethrown error keep the original attached instead of swallowing it. Check how you pass it, read it back, and what it does — and doesn't — support.",{"questionCount":39,"timeLimitSec":40,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":41,"maxAttempts":40,"revealAnswers":42,"allowFlagging":18,"allowBacktracking":17},6,null,70,"IMMEDIATE",{"slug":6,"name":7},{"questions":39},{"allowed":17,"reason":46},"FREE",[],{"id":49,"slug":34,"title":50,"subtitle":40,"excerpt":51,"coverUrl":52,"locale":13,"readingMinutes":53,"publishedAt":54,"viewCount":55,"likeCount":19,"commentCount":19,"author":56,"vertical":61,"topic":62,"tags":65,"_count":77,"playground":79,"body":81,"bodyMd":249,"seo":250,"translationGroupId":253,"thread":254,"assessments":256,"translations":259,"quiz":261},"019fe660-5f55-701e-9cf4-dc0c9f0517b9","You're rethrowing errors and losing context. `Error.cause` fixes that.","Every time you catch an error and rethrow a new one without forwarding the original, you lose the stack trace, the error type, and everything useful about what actually went wrong. ES2022 added `Error.cause` to fix exactly this.","\u002Fmedia\u002Fcovers\u002Ferror-cause-rethrow-context.png",4,"2026-07-21T08:36:02.331Z",31,{"id":57,"name":58,"username":59,"avatarUrl":40,"headline":60},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":63,"name":64},"javascript","JavaScript",[66,68,71,74],{"slug":63,"name":67,"color":40},"Javascript",{"slug":69,"name":70,"color":40},"webdev","Webdev",{"slug":72,"name":73,"color":40},"node","Node",{"slug":75,"name":76,"color":40},"typescript","Typescript",{"assessments":78},1,{"slug":34,"title":80},"Error.cause — interactive playground",{"blocks":82,"version":78},[83,87,93,96,99,104,107,111,114,118,121,124,127,130,133,136,139,142,146,149,152,156,159,162,165,169,172,175,178,183,186,190,194,197,201,204,207,210,213,216,219,222,225,228,231,234,237,240],{"id":84,"html":85,"type":86},"b1","\u003Cp>Error handling has a quiet problem. You catch an error deep in a call stack, wrap it in something more descriptive, and rethrow. The caller gets a meaningful message — but the original error, with its stack trace and details, is gone.\u003C\u002Fp>","paragraph",{"id":88,"code":89,"type":90,"language":91,"highlight":92},"b2","async function loadUser(id) {\n  try {\n    const res = await fetch(`\u002Fapi\u002Fusers\u002F${id}`);\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return await res.json();\n  } catch (err) {\n    throw new Error(`Failed to load user ${id}`); \u002F\u002F original err swallowed\n  }\n}","code","js",[],{"id":94,"html":95,"type":86},"b3","\u003Cp>Your logs now say \u003Ccode>Failed to load user 42\u003C\u002Fcode> and nothing else. Was it a network timeout? A 403? A JSON parse error? You don&#39;t know unless you explicitly log before rethrowing — which most people remember only after the third unexplained production incident.\u003C\u002Fp>",{"id":97,"html":98,"type":86},"b4","\u003Cp>ES2022 added the fix: the \u003Ccode>cause\u003C\u002Fcode> option on \u003Ccode>Error\u003C\u002Fcode>.\u003C\u002Fp>",{"id":100,"html":101,"text":102,"type":103,"level":31},"b5","How \u003Ccode>Error.cause\u003C\u002Fcode> works","How Error.cause works","heading",{"id":105,"html":106,"type":86},"b6","\u003Cp>Every \u003Ccode>Error\u003C\u002Fcode> constructor accepts an optional second argument: an \u003Ccode>options\u003C\u002Fcode> object. Set \u003Ccode>cause\u003C\u002Fcode> to the original error and it&#39;s preserved on the new error:\u003C\u002Fp>",{"id":108,"code":109,"type":90,"language":91,"highlight":110},"b7","async function loadUser(id) {\n  try {\n    const res = await fetch(`\u002Fapi\u002Fusers\u002F${id}`);\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return await res.json();\n  } catch (err) {\n    throw new Error(`Failed to load user ${id}`, { cause: err });\n  }\n}",[],{"id":112,"html":113,"type":86},"b8","\u003Cp>Now the new error carries the original as \u003Ccode>err.cause\u003C\u002Fcode>. When the caller catches it, both levels are available:\u003C\u002Fp>",{"id":115,"code":116,"type":90,"language":91,"highlight":117},"b9","try {\n  await loadUser(42);\n} catch (err) {\n  console.error(err.message);       \u002F\u002F \"Failed to load user 42\"\n  console.error(err.cause.message); \u002F\u002F \"HTTP 403\"\n  console.error(err.cause);         \u002F\u002F the original Error with its full stack trace\n}",[],{"id":119,"html":120,"type":86},"b10","\u003Cp>The original error isn&#39;t gone — it&#39;s attached. One property to read when you need it, invisible when you don&#39;t.\u003C\u002Fp>",{"id":122,"html":123,"type":86},"b11","\u003C!-- playground:start -->",{"id":125,"html":126,"text":126,"type":103,"level":31},"b12","🎮 Try it yourself",{"id":128,"html":129,"type":86},"b13","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-21-error-cause-rethrow-context\u002Fplayground\u002F\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":131,"html":132,"type":86},"b14","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":134,"html":135,"type":86},"b15","\u003C!-- playground:end -->",{"id":137,"html":138,"text":138,"type":103,"level":31},"b16","Error chains instead of error strings",{"id":140,"html":141,"type":86},"b17","\u003Cp>The old workaround was to concatenate messages:\u003C\u002Fp>",{"id":143,"code":144,"type":90,"language":91,"highlight":145},"b18","throw new Error(`Failed to load user ${id}: ${err.message}`);",[],{"id":147,"html":148,"type":86},"b19","\u003Cp>This preserves the message text — but only the text. The stack trace of the original error disappears. The error type disappears. If the original error had a cause of its own, that disappears too.\u003C\u002Fp>",{"id":150,"html":151,"type":86},"b20","\u003Cp>With \u003Ccode>Error.cause\u003C\u002Fcode>, you get a chain, not a flattened string:\u003C\u002Fp>",{"id":153,"code":154,"type":90,"language":91,"highlight":155},"b21","\u002F\u002F Higher up the stack\ntry {\n  await initDashboard();\n} catch (err) {\n  console.error(err.message);             \u002F\u002F \"Dashboard init failed\"\n  console.error(err.cause.message);       \u002F\u002F \"Failed to load user 42\"\n  console.error(err.cause.cause.message); \u002F\u002F \"HTTP 403\"\n}",[],{"id":157,"html":158,"type":86},"b22","\u003Cp>Every layer adds context. None of them destroy what came before.\u003C\u002Fp>",{"id":160,"html":161,"text":161,"type":103,"level":31},"b23","A real-world pattern: the service layer",{"id":163,"html":164,"type":86},"b24","\u003Cp>The clearest use case is a service layer wrapping raw API calls:\u003C\u002Fp>",{"id":166,"code":167,"type":90,"language":91,"highlight":168},"b25","class UserService {\n  async getUser(id) {\n    try {\n      const raw = await this.api.get(`\u002Fusers\u002F${id}`);\n      return User.from(raw);\n    } catch (err) {\n      throw new Error(`UserService.getUser(${id}) failed`, { cause: err });\n    }\n  }\n}",[],{"id":170,"html":171,"type":86},"b26","\u003Cp>The caller gets a domain error (\u003Ccode>UserService.getUser(42) failed\u003C\u002Fcode>) without losing the infrastructure detail (\u003Ccode>HTTP 403\u003C\u002Fcode>, \u003Ccode>ECONNREFUSED\u003C\u002Fcode>, \u003Ccode>SyntaxError: Unexpected token\u003C\u002Fcode>). A logging layer can walk the \u003Ccode>.cause\u003C\u002Fcode> chain to emit structured logs at each level. Error monitoring tools that understand cause chains — Sentry does — can render the full tree as a linked sequence rather than a flattened string.\u003C\u002Fp>",{"id":173,"html":174,"text":174,"type":103,"level":31},"b27","TypeScript support",{"id":176,"html":177,"type":86},"b28","\u003Cp>TypeScript added the \u003Ccode>ErrorOptions\u003C\u002Fcode> type in 4.6. The \u003Ccode>cause\u003C\u002Fcode> field is typed as \u003Ccode>unknown\u003C\u002Fcode>, which is correct — any value can be a cause, not just \u003Ccode>Error\u003C\u002Fcode> instances:\u003C\u002Fp>",{"id":179,"code":180,"type":90,"language":181,"highlight":182},"b29","throw new Error('Operation failed', { cause: err });\n\u002F\u002F err.cause is typed as unknown — narrow it before using\n\nif (err.cause instanceof Error) {\n  console.error(err.cause.message); \u002F\u002F ✅ safe\n}","ts",[],{"id":184,"html":185,"type":86},"b30","\u003Cp>Custom error classes work the same way — pass \u003Ccode>options\u003C\u002Fcode> through to \u003Ccode>super\u003C\u002Fcode> and the base \u003Ccode>Error\u003C\u002Fcode> constructor populates \u003Ccode>this.cause\u003C\u002Fcode> automatically:\u003C\u002Fp>",{"id":187,"code":188,"type":90,"language":181,"highlight":189},"b31","class ApiError extends Error {\n  constructor(message: string, options?: ErrorOptions) {\n    super(message, options);\n    this.name = 'ApiError';\n  }\n}\n\nthrow new ApiError('Request failed', { cause: originalError });",[],{"id":191,"html":192,"text":193,"type":103,"level":31},"b32","\u003Ccode>cause\u003C\u002Fcode> doesn&#39;t have to be an Error","cause doesn't have to be an Error",{"id":195,"html":196,"type":86},"b33","\u003Cp>\u003Ccode>cause\u003C\u002Fcode> accepts any value. If what went wrong was a failed validation, a non-Error rejected promise, or a raw HTTP response object, attach it directly:\u003C\u002Fp>",{"id":198,"code":199,"type":90,"language":91,"highlight":200},"b34","throw new Error('Invalid configuration', {\n  cause: { field: 'timeout', received: -1, expected: '>0' },\n});",[],{"id":202,"html":203,"type":86},"b35","\u003Cp>\u003Ccode>err.cause\u003C\u002Fcode> holds the original object — not a stringified version of it. That&#39;s more useful than trying to serialize context into a message string and more structured than console-logging separately before rethrowing.\u003C\u002Fp>",{"id":205,"html":206,"text":206,"type":103,"level":31},"b36","Browser support",{"id":208,"html":209,"type":86},"b37","\u003Cp>\u003Ccode>Error\u003C\u002Fcode> options including \u003Ccode>cause\u003C\u002Fcode> are \u003Cstrong>Baseline 2022\u003C\u002Fstrong>: Chrome 93, Firefox 91, Safari 15.4, Node.js 16.9. Every actively maintained browser and runtime ships it. There is nothing to install and nothing to polyfill; the only thing to change is the habit.\u003C\u002Fp>",{"id":211,"html":212,"type":86},"b38","\u003C!-- quiz:start -->",{"id":214,"html":215,"text":215,"type":103,"level":31},"b39","🧠 Test yourself",{"id":217,"html":218,"type":86},"b40","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-21-error-cause-rethrow-context\">Take the 6-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":220,"html":221,"type":86},"b41","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":223,"html":224,"type":86},"b42","\u003C!-- quiz:end -->",{"id":226,"html":227,"text":227,"type":103,"level":31},"b43","The takeaway",{"id":229,"html":230,"type":86},"b44","\u003Cp>Search your codebase for \u003Ccode>catch (err) { throw new Error(\u003C\u002Fcode> and look at each one. Where the catch clause doesn&#39;t forward \u003Ccode>err\u003C\u002Fcode>, it&#39;s swallowing context someone will want the next time that error appears in production.\u003C\u002Fp>",{"id":232,"html":233,"type":86},"b45","\u003Cp>Pass \u003Ccode>{ cause: err }\u003C\u002Fcode> as the second argument to \u003Ccode>Error()\u003C\u002Fcode> and the original error stops disappearing. The message is what you put in the error. The cause is what actually went wrong underneath. They belong in the same object.\u003C\u002Fp>",{"id":235,"type":236},"b46","divider",{"id":238,"html":239,"type":86},"b47","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":241,"type":242,"items":243,"ordered":18},"b48","list",[244,245,246,247,248],"⭐ \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>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","Error handling has a quiet problem. You catch an error deep in a call stack, wrap it in something more descriptive, and rethrow. The caller gets a meaningful message — but the original error, with its stack trace and details, is gone.\n\n```js\nasync function loadUser(id) {\n  try {\n    const res = await fetch(`\u002Fapi\u002Fusers\u002F${id}`);\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return await res.json();\n  } catch (err) {\n    throw new Error(`Failed to load user ${id}`); \u002F\u002F original err swallowed\n  }\n}\n```\n\nYour logs now say `Failed to load user 42` and nothing else. Was it a network timeout? A 403? A JSON parse error? You don't know unless you explicitly log before rethrowing — which most people remember only after the third unexplained production incident.\n\nES2022 added the fix: the `cause` option on `Error`.\n\n## How `Error.cause` works\n\nEvery `Error` constructor accepts an optional second argument: an `options` object. Set `cause` to the original error and it's preserved on the new error:\n\n```js\nasync function loadUser(id) {\n  try {\n    const res = await fetch(`\u002Fapi\u002Fusers\u002F${id}`);\n    if (!res.ok) throw new Error(`HTTP ${res.status}`);\n    return await res.json();\n  } catch (err) {\n    throw new Error(`Failed to load user ${id}`, { cause: err });\n  }\n}\n```\n\nNow the new error carries the original as `err.cause`. When the caller catches it, both levels are available:\n\n```js\ntry {\n  await loadUser(42);\n} catch (err) {\n  console.error(err.message);       \u002F\u002F \"Failed to load user 42\"\n  console.error(err.cause.message); \u002F\u002F \"HTTP 403\"\n  console.error(err.cause);         \u002F\u002F the original Error with its full stack trace\n}\n```\n\nThe original error isn't gone — it's attached. One property to read when you need it, invisible when you don't.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-21-error-cause-rethrow-context\u002Fplayground\u002F)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Error chains instead of error strings\n\nThe old workaround was to concatenate messages:\n\n```js\nthrow new Error(`Failed to load user ${id}: ${err.message}`);\n```\n\nThis preserves the message text — but only the text. The stack trace of the original error disappears. The error type disappears. If the original error had a cause of its own, that disappears too.\n\nWith `Error.cause`, you get a chain, not a flattened string:\n\n```js\n\u002F\u002F Higher up the stack\ntry {\n  await initDashboard();\n} catch (err) {\n  console.error(err.message);             \u002F\u002F \"Dashboard init failed\"\n  console.error(err.cause.message);       \u002F\u002F \"Failed to load user 42\"\n  console.error(err.cause.cause.message); \u002F\u002F \"HTTP 403\"\n}\n```\n\nEvery layer adds context. None of them destroy what came before.\n\n## A real-world pattern: the service layer\n\nThe clearest use case is a service layer wrapping raw API calls:\n\n```js\nclass UserService {\n  async getUser(id) {\n    try {\n      const raw = await this.api.get(`\u002Fusers\u002F${id}`);\n      return User.from(raw);\n    } catch (err) {\n      throw new Error(`UserService.getUser(${id}) failed`, { cause: err });\n    }\n  }\n}\n```\n\nThe caller gets a domain error (`UserService.getUser(42) failed`) without losing the infrastructure detail (`HTTP 403`, `ECONNREFUSED`, `SyntaxError: Unexpected token`). A logging layer can walk the `.cause` chain to emit structured logs at each level. Error monitoring tools that understand cause chains — Sentry does — can render the full tree as a linked sequence rather than a flattened string.\n\n## TypeScript support\n\nTypeScript added the `ErrorOptions` type in 4.6. The `cause` field is typed as `unknown`, which is correct — any value can be a cause, not just `Error` instances:\n\n```ts\nthrow new Error('Operation failed', { cause: err });\n\u002F\u002F err.cause is typed as unknown — narrow it before using\n\nif (err.cause instanceof Error) {\n  console.error(err.cause.message); \u002F\u002F ✅ safe\n}\n```\n\nCustom error classes work the same way — pass `options` through to `super` and the base `Error` constructor populates `this.cause` automatically:\n\n```ts\nclass ApiError extends Error {\n  constructor(message: string, options?: ErrorOptions) {\n    super(message, options);\n    this.name = 'ApiError';\n  }\n}\n\nthrow new ApiError('Request failed', { cause: originalError });\n```\n\n## `cause` doesn't have to be an Error\n\n`cause` accepts any value. If what went wrong was a failed validation, a non-Error rejected promise, or a raw HTTP response object, attach it directly:\n\n```js\nthrow new Error('Invalid configuration', {\n  cause: { field: 'timeout', received: -1, expected: '>0' },\n});\n```\n\n`err.cause` holds the original object — not a stringified version of it. That's more useful than trying to serialize context into a message string and more structured than console-logging separately before rethrowing.\n\n## Browser support\n\n`Error` options including `cause` are **Baseline 2022**: Chrome 93, Firefox 91, Safari 15.4, Node.js 16.9. Every actively maintained browser and runtime ships it. There is nothing to install and nothing to polyfill; the only thing to change is the habit.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 6-question quiz →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-21-error-cause-rethrow-context)**\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## The takeaway\n\nSearch your codebase for `catch (err) { throw new Error(` and look at each one. Where the catch clause doesn't forward `err`, it's swallowing context someone will want the next time that error appears in production.\n\nPass `{ cause: err }` as the second argument to `Error()` and the original error stops disappearing. The message is what you put in the error. The cause is what actually went wrong underneath. They belong in the same object.\n\n---\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)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":50,"canonical":251,"description":252},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Ferror-cause-rethrow-context","Every time you catch an error and rethrow a new one without forwarding the original, you lose the stack trace, the error type, and everything useful about what actually went wrong.","019fe660-5f55-701e-9cf4-e334a249972c",{"id":255,"locked":18},"019fe660-664b-77ee-85fa-7566c627f091",[257],{"id":33,"slug":34,"title":36,"_count":258},{"questions":39},[260],{"locale":13,"slug":34},{"id":33,"slug":34,"title":36,"_count":262,"questionCount":39},{"questions":39}]