Stockio

TypeScript Error Handling: unknown, Causes and Results

Handle failures in TypeScript with unknown catch variables, useful Error subclasses, explicit result unions, and clear ownership of logging and recovery decisions.

By Daniel Kim

React Developer ·

On this page

A catch block is a boundary where control arrives without a reliable description of what went wrong. JavaScript permits throwing strings, objects, and even undefined. Effective TypeScript error handling starts by admitting that uncertainty, then deciding which layer can recover and which layer should explain the failure to a person.

Separate expected outcomes from unexpected failures

A missing optional preference is usually not the same kind of event as corrupted persisted data. Before introducing custom errors, list the outcomes callers genuinely need to distinguish. An empty search result might be normal data; an invalid saved document may require a recovery screen.

Use the JSON formatter with sanitized examples of successful and failed payloads. Compare the fields callers actually inspect. This helps design an error contract, but formatted JSON does not establish that a value is trustworthy or that a server status represents success.

Assign responsibility deliberately. A low-level parser knows why parsing failed, a service function knows which operation was attempted, and a view knows how to offer a retry. If every layer logs and rewrites the same error, the original problem becomes harder to diagnose.

Narrow catch variables instead of asserting them

Enable strict TypeScript settings so catch variables are treated as unknown unless configured otherwise. Unknown requires a check before reading a message. This matches JavaScript's runtime behavior rather than assuming every thrown value inherits from Error.

function errorMessage(value: unknown): string {
if (value instanceof Error) return value.message;
if (typeof value === "string") return value;
return "An unexpected failure occurred";
}

async function loadDocument(url: string): Promise<unknown> {
const response = await fetch(url);
if (!response.ok) {
  throw new Error("Document request failed: " + response.status);
}
return response.json();
}

A fetch promise can fulfill with an HTTP error response, so the explicit status check matters. JSON decoding can also fail after the response arrives. Do not conflate transport success, acceptable status, successful parsing, and valid application data.

The TypeScript narrowing guide covers the checks that turn unknown values into usable types. Avoid a broad as Error cast merely to access message. It hides the uncertainty without changing runtime behavior.

Add context while preserving the cause

A low-level message such as unexpected token does not tell an operator which user action failed. Wrap it with operation context while preserving the original error as a cause. Current runtimes supporting Error causes retain the causal chain for tools that inspect it.

async function readPreferences(url: string): Promise<unknown> {
try {
  return await loadDocument(url);
} catch (cause: unknown) {
  throw new Error("Could not load saved preferences", { cause });
}
}

The await inside this try block is intentional. Returning the promise without awaiting it would allow a later rejection to escape this catch. Conversely, do not surround synchronous setup with a catch and assume it will catch a promise rejection that nobody awaits.

The MDN Error reference documents causes and other standard properties. Check your compilation library and runtime support before relying on newer constructor options. A transpiler does not automatically provide every runtime feature.

Use causal context, not secret context. Avoid placing authentication tokens, full response bodies, or private document contents in messages that might reach logs or users. A stable operation name and safe identifier are usually more useful than an unfiltered payload.

Represent recoverable alternatives with results

When a caller is expected to branch on a failure, a discriminated result can make that obligation visible. Throwing remains useful for exceptional failures, but a result type can describe predictable outcomes such as a missing local document or unsupported file version.

type ReadResult<T> =
| { ok: true; value: T }
| { ok: false; reason: "missing" | "unsupported" };

function readTitle(result: ReadResult<{ title: string }>): string {
if (!result.ok) return "Document unavailable";
return result.value.title;
}

Do not catch every exception and convert it into missing. That would disguise programmer mistakes and make corrupted data indistinguishable from an absent file. Keep the failure taxonomy small enough that each category has a concrete recovery behavior.

A result type also does not ensure callers inspect it. They can ignore any returned value. Its benefit is that code accessing the success payload must narrow first. Runtime tests should confirm that each documented category is produced under the intended conditions.

Put recovery and reporting at the right layer

Choose a single reporting boundary for an operation. Lower layers can enrich errors, but the boundary should decide whether to log, retry, show a message, or escalate. This avoids duplicate console entries and multiple toasts for one failed request.

Always observe promises started by event handlers. A deliberate fire-and-forget operation still needs a rejection handler if rejection is possible. The MDN promise guide explains how errors propagate through chains and async functions.

Use finally for resource cleanup, not for returning fallback values. A return or throw inside finally can override the original outcome, making failures disappear. Test cleanup when the main operation succeeds, rejects, and is cancelled. Cancellation may deserve neutral presentation instead of an error notification.

Include tests for a thrown string, malformed JSON, a rejected fetch, and an HTTP failure with a valid JSON body. Keep these fixtures with your other language examples, and document reporting ownership in your development guidance. A strong error policy lets someone trace a failed interaction from the visible message to its original cause without guessing which layer swallowed the evidence.

Make retry decisions specific to the operation

Consider saving a document. A validation failure needs an actionable explanation, not an automatic retry. An authentication failure may require signing in again. A transient connection failure may justify offering retry, but only after considering whether the original write could already have reached the server. Repeating an operation is a behavior decision, not a generic response to every caught exception.

Keep stable machine-readable categories separate from display messages. Branching on whether a message contains a particular English phrase breaks when text changes or localization is added. A small error code or result discriminant gives callers a stable decision point, while the message remains free to explain the problem clearly. Avoid exposing internal stack traces as user guidance.

Test recovery ownership with one end-to-end scenario: the parser fails, the service adds operation context, and the screen offers a safe next step. Assert that only one report is emitted and that the original cause remains inspectable. Then repeat with an expected missing-document result and verify that it does not trigger the same alarm as corrupted data.

If errors cross a worker or network boundary, do not assume the original Error prototype and custom properties survive the transfer. Define a serializable error envelope containing only approved fields, validate it on receipt, and map it into local recovery behavior. This boundary is also a useful place to remove sensitive details. Error handling is successful when it supports a correct next action, not merely when every exception has been converted into a string.

Key takeaways

  • Treat caught values as unknown until checked.
  • Distinguish expected alternatives from exceptional failures.
  • Preserve causes when adding operation context.
  • Observe asynchronous rejections explicitly.
  • Give one layer responsibility for recovery and reporting.
Featured sponsor

Reach designers & developers at work

Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.

Become a sponsor

Keep reading

Related articles

More in JavaScript & Frameworks
JavaScript & Frameworks

10 Best JavaScript Charting Libraries

Compare ten JavaScript chart libraries for dashboards, reports, and interactive data stories. See where each excels, what it costs, and how to choose based on your app and team.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

8 Best React State Management Libraries

Find a React state approach that fits your app, from built-in hooks to dedicated stores, with clear trade-offs around data flow, debugging, async work, and learning effort.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

12 Best and Most Popular JavaScript Libraries

Compare twelve widely used JavaScript libraries for interfaces, data, testing, and everyday development, with clear strengths, trade-offs, and advice for choosing only what your project needs.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

localStorage vs sessionStorage vs IndexedDB Compared

Choose browser storage by data lifetime, size and access pattern. Compare synchronous Web Storage with IndexedDB transactions, migrations, quotas and failure handling.

Includes: JSON formatterSofia MarchettiSofia Marchetti
JavaScript & Frameworks

React Server Components Explained for Developers

Learn what React Server Components execute, what crosses the client boundary, and how to structure data access, interactive widgets, and mutations without leaking private data.

Includes: JSON formatterDaniel KimDaniel Kim
JavaScript & Frameworks

useMemo vs useCallback vs React.memo: Fewer Rerenders

Learn when React memoization helps, how prop identity affects rerenders, and why profiling and state placement should come before useMemo and useCallback.

Includes: JSON formatterDaniel KimDaniel Kim