{"post":{"slug":"error-handling-explained-2026","title":"Error handling explained in 2026: stop letting exceptions crash your users","excerpt":"Poor error handling is the leading cause of poor user experience and silent data corruption. This 2026 guide covers strategies, typed errors, and production patterns.","category":"code","date":"March 27, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">E</span>rror handling is the part of software that separates a prototype from a production system. A prototype crashes and you restart it; a production system degrades gracefully, retries the right things, tells users what went wrong, and gives engineers the context to fix it. Getting there requires intentional decisions about what errors mean, where to handle them, and how to communicate them — not just wrapping everything in `try/catch` and hoping.\n\n## What changed in 2026\n\n- **TypeScript `using` declarations (ES2023, now universal)** ensure cleanup on scope exit even when exceptions throw, replacing many manual `try/finally` patterns.\n- **Node.js 22 made unhandled promise rejections fatal** — silent promise failures that previously swallowed errors now crash the process by default.\n- **Rust-style Result types gained popularity in TypeScript** via libraries like `neverthrow` and `oxide-ts` — making error paths visible in function signatures.\n- **OpenTelemetry became the standard** for propagating error context through distributed systems; structured error metadata flows through traces automatically.\n\n## Operational vs programmer errors\n\n| Type | Meaning | Response |\n| --- | --- | --- |\n| **Operational error** | Expected failure in a running system | Handle, retry, or degrade gracefully |\n| **Programmer error** | Bug — the code did something wrong | Fix the code; do not catch and continue |\n\nExamples of operational errors: network timeout, 404 from an upstream API, database connection refused, user input validation failure, file not found.\n\nExamples of programmer errors: `TypeError: Cannot read properties of undefined`, wrong arguments passed to a function, assertion violation.\n\n**Never \"handle\" a programmer error by swallowing it.** Let it crash and fix the bug.\n\n## The golden rules\n\n```ts\n// WRONG — empty catch is always wrong\ntry {\n  await db.save(order);\n} catch (err) {} // silent data loss\n\n// WRONG — catch without context\ntry {\n  await processPayment(charge);\n} catch (err) {\n  throw new Error(\"Payment failed\"); // stack trace is gone\n}\n\n// RIGHT — log with context, rethrow with cause\ntry {\n  await processPayment(charge);\n} catch (err) {\n  logger.error({ chargeId: charge.id, err }, \"payment failed\");\n  throw new PaymentError(\"Payment processing failed\", { cause: err });\n}\n```\n\n## Typed errors in TypeScript\n\nCustom error classes make error types explicit and catchable:\n\n```ts\nclass NotFoundError extends Error {\n  constructor(\n    public readonly resource: string,\n    public readonly id: string,\n  ) {\n    super(`${resource} ${id} not found`);\n    this.name = \"NotFoundError\";\n  }\n}\n\nclass ValidationError extends Error {\n  constructor(public readonly fields: Record<string, string>) {\n    super(\"Validation failed\");\n    this.name = \"ValidationError\";\n  }\n}\n\n// Caller can discriminate\ntry {\n  const user = await userService.findById(id);\n} catch (err) {\n  if (err instanceof NotFoundError) return res.status(404).json({ error: err.message });\n  if (err instanceof ValidationError) return res.status(400).json({ errors: err.fields });\n  throw err; // unknown errors propagate\n}\n```\n\n## Result type pattern\n\nFor functions that frequently fail, a `Result<T, E>` type makes the error path visible without exceptions:\n\n```ts\ntype Ok<T> = { ok: true; value: T };\ntype Err<E> = { ok: false; error: E };\ntype Result<T, E> = Ok<T> | Err<E>;\n\nasync function parseConfig(raw: string): Promise<Result<Config, string>> {\n  try {\n    return { ok: true, value: JSON.parse(raw) };\n  } catch {\n    return { ok: false, error: \"Invalid JSON in config\" };\n  }\n}\n\nconst result = await parseConfig(input);\nif (!result.ok) {\n  console.error(result.error);\n  process.exit(1);\n}\n// result.value is narrowed to Config here\n```\n\n## Python error handling\n\n```python\n# Custom exception hierarchy\nclass AppError(Exception):\n    pass\n\nclass NotFoundError(AppError):\n    def __init__(self, resource: str, id: str):\n        super().__init__(f\"{resource} {id} not found\")\n        self.resource = resource\n        self.id = id\n\n# Proper re-raise with context\ntry:\n    user = await user_repo.find(user_id)\nexcept DatabaseError as exc:\n    raise NotFoundError(\"User\", user_id) from exc  # preserves original traceback\n```\n\n## How to pick\n\n1. **Recoverable, expected failure** — catch, log with context, return a fallback or retry.\n2. **Error that changes the response** — catch at the HTTP boundary, map to the right status code.\n3. **Bug / programmer error** — let it crash; do not catch.\n4. **Function that frequently returns \"not found\" or \"invalid\"** — consider a Result type.\n\n## Common mistakes\n\n**Catching `Error` and re-throwing a new `Error` without `{ cause }`** — the original stack trace is lost.\n\n**Catching in every function.** Catch at the boundary where you can take action. Intermediate functions should let errors propagate.\n\n**Generic error messages.** \"Something went wrong\" is useless to both users and engineers. Include the resource, the id, and the reason.\n\n**Logging after rethrowing (double-logging).** Log once — either here if you handle it, or at the top-level boundary if you rethrow.\n\n## What to skip\n\n- **try/catch around every `await`** — unhandled rejections now crash the process, which is usually what you want for programmer errors.\n- **Catch-all middleware that returns 500 for known operational errors** — map known error types to appropriate status codes upstream.\n- **Error codes as magic numbers** — use descriptive string constants or enum members.\n\n## FAQ\n\n**Should I use error codes or error messages?**\nBoth. A machine-readable code (`NOT_FOUND`, `PAYMENT_DECLINED`) for programmatic handling; a human-readable message for logs and user interfaces.\n\n**What is `error.cause` in JavaScript?**\n`new Error(\"message\", { cause: originalError })` chains errors without losing the original stack. Accessible as `err.cause`. Supported natively since Node.js 16.9.\n\n**How do I handle errors in async event handlers?**\nAsync event handlers that throw are unhandled by default. Wrap with an explicit try/catch and emit to an error handler: `emitter.on(\"data\", async (d) => { try { await process(d); } catch (err) { emitter.emit(\"error\", err); } })`.\n\n**What is the difference between throw and reject?**\nIn async code, throwing inside an async function rejects the returned promise. They are equivalent. Prefer `throw` for consistency with synchronous style.\n\n## Where to go next\n\nSee [Logging explained in 2026](/blog/logging-explained-2026), [Async/await explained in 2026](/blog/async-await-explained-2026), and [How to handle errors gracefully in 2026](/blog/how-to-handle-errors-gracefully-2026).","hue":309,"metaTitle":"Error Handling Explained 2026: Stop Crashing Users","metaDescription":"Complete 2026 guide to error handling in JavaScript and Python. Typed errors, Result types, operational vs programmer errors, and production patterns.","keywords":["error handling explained 2026","how to handle errors gracefully 2026","typed errors typescript","result type error handling","operational vs programmer errors","error handling best practices","try catch 2026","python error handling 2026","express error middleware","never swallow errors"],"tldr":["**Handle errors at the right level** — catch where you can recover; let errors propagate where you cannot.","**Never swallow errors silently** — an empty catch block is always wrong; log or rethrow.","**Use typed errors or Result types** — callers know what can fail and are forced to handle it.","**Distinguish operational errors from programmer errors** — operational errors (network timeout) are expected; programmer errors (null pointer) are bugs.","**Skip:** catching `Error` at every function boundary — this defeats propagation and buries stack traces."],"readTime":"4 min read","wordCount":959},"related":[{"slug":"how-to-handle-errors-gracefully-2026","title":"How to handle errors gracefully in 2026: patterns for resilient services","excerpt":"Error handling is the difference between a system that fails safely and one that fails silently or catastrophically. This 2026 guide covers error types, propagation patterns, and the practices that make services observable and resilient.","category":"code","date":"March 31, 2026","author":"ByteLedger Team","readTime":"6 min read","hue":320},{"slug":"how-to-handle-api-errors-2026","title":"How to handle API errors in 2026","excerpt":"Unhandled API errors are the leading cause of silent data loss and broken UX. This 2026 guide covers every layer from network faults to typed error objects.","category":"code","date":"March 31, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":319},{"slug":"how-to-build-a-rest-api-2026","title":"How to build a REST API in 2026: the complete guide","excerpt":"REST APIs are still the backbone of most web backends in 2026. This guide covers design, implementation, validation, auth, and deployment correctly.","category":"code","date":"April 14, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":322}],"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":"django-vs-flask-2026","title":"Django vs Flask in 2026: choosing the right Python web framework","excerpt":"Django ships a full web stack; Flask ships a request/response core and lets you compose the rest. Here is the 2026 guide to choosing between them.","category":"code","date":"March 27, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":311},"older":{"slug":"fastapi-vs-flask-2026","title":"FastAPI vs Flask in 2026: which Python web framework wins?","excerpt":"Flask is still the simplest Python web framework, but FastAPI now dominates new Python API projects. Here is when each one still makes sense.","category":"code","date":"March 27, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":314}}