DEV Community

Cover image for TypeScript `@throws` JSDoc and Error Typing in 2026: Why Exceptions Are Still Untyped and What to Do About It
jsmanifest
jsmanifest

Posted on Originally published at jsmanifest.com

TypeScript `@throws` JSDoc and Error Typing in 2026: Why Exceptions Are Still Untyped and What to Do About It

TypeScript @throws JSDoc and Error Typing in 2026: Why Exceptions Are Still Untyped and What to Do About It

This article was written with the assistance of AI, under human supervision and review.

Developers write try/catch blocks around TypeScript functions without knowing what exceptions those functions throw. The compiler stays silent when a function that throws NetworkError gets called without a handler, and the codebase ships with runtime crashes that static analysis should have caught. This happens because TypeScript treats exceptions as invisible to the type system.

Problem flow showing unhandled exception propagation in TypeScript

Problem flow showing unhandled exception propagation in TypeScript

Languages with checked exceptions force developers to declare and handle throws behavior at compile time. TypeScript documentation includes a @throws JSDoc tag, but the tag has no enforcement mechanism. Engineers who write @throws {NetworkError} above a function signature get zero compiler feedback when callers ignore that annotation. The gap between documentation and verification creates a dangerous false confidence.

Solution flow showing explicit Result type error handling

Solution flow showing explicit Result type error handling

Teams that need predictable error handling in TypeScript codebases must build their own type-safe patterns. Result types, discriminated unions, and explicit error boundaries provide the guarantees the language does not. The solutions require more code than a hypothetical throws keyword, but they work today without waiting for a language feature that may never arrive.

Key Takeaways

  • TypeScript does not type exceptions and likely never will due to JavaScript runtime semantics and the unchecked exception model.
  • The @throws JSDoc tag documents throw behavior but provides zero compiler enforcement or type safety.
  • Result types (Result<T, E>) and discriminated unions create verifiable error handling without language-level throw types.
  • Error discrimination patterns using instanceof and type guards allow safe exception handling in catch blocks when exceptions remain the chosen approach.
  • Teams must choose between exceptions with manual documentation (faster to write, harder to verify) and Result types (more verbose, compiler-verified).

Why TypeScript Still Doesn't Type Exceptions in 2026

TypeScript inherits JavaScript's unchecked exception model. Any function can throw any value at any time, and the runtime provides no compile-time guarantees about what gets thrown. Java and C# have checked exceptions where the compiler enforces throw declarations, but JavaScript does not, and TypeScript follows JavaScript semantics.

The language could theoretically add throw types as an optional static analysis layer. TypeScript already diverges from JavaScript in ways that require compilation (enums, namespaces before TypeScript 5.8's --erasableSyntaxOnly). Adding a throws keyword or enforcing @throws annotations would fit that pattern. The TypeScript team has chosen not to pursue this feature.

Three technical barriers prevent practical implementation of throw types in TypeScript:

Dynamic function references lose throw information under indirection. When a function gets assigned to a variable, passed as a callback, or returned from another function, TypeScript would need to preserve throw signatures through every layer of indirection. Higher-order functions and async operations create combinatorial explosion in the number of possible throw paths.

Third-party code has no throw annotations. The vast majority of npm packages ship without throw documentation. Even if TypeScript added throw types tomorrow, existing libraries would appear to throw nothing. The type system would require every developer to manually annotate every dependency, or accept a mass of implicit any exceptions that defeat the entire purpose.

JavaScript's throw accepts any type. Code can throw strings, numbers, null, or custom objects. A robust throw type system would need to model all possible thrown values, including edge cases where throw appears in expression position. The complexity of accurately representing JavaScript's throw semantics exceeds the benefit.

TypeScript 5.2 added decorator metadata (accessed via Symbol.metadata) that frameworks use to attach runtime information to classes. Decorator metadata does not extend to function throw signatures. TypeScript 5.8 introduced --erasableSyntaxOnly to restrict syntax to patterns that Node.js type-stripping can handle, but this targets import/export semantics and namespace elimination, not exception typing.

The official TypeScript handbook documents @throws in JSDoc but provides no mechanism to verify those annotations. The tag exists for human readers and documentation generators, not for the compiler.

The @throws JSDoc Tag: What It Provides and What It Doesn't

TypeScript recognizes the @throws JSDoc tag as valid documentation syntax. Developers write @throws {ErrorType} description above function declarations to document exception behavior. IDEs display this information in hover tooltips and generated documentation sites parse it. The compiler ignores it completely during type checking.

/**
 * Fetches user data from the API
 * @throws {NetworkError} when the request fails
 * @throws {ValidationError} when the response is invalid
 */
function fetchUser(id: string): Promise<User> {
  // implementation
}

// This compiles without error despite missing try/catch
const user = await fetchUser("123");
Enter fullscreen mode Exit fullscreen mode

The function signature carries throw information in JSDoc but the call site receives no type error. Developers who read the hover tooltip see the documented exceptions. Developers who skip the tooltip get no warning when they omit error handling. The pattern depends on discipline rather than verification.

JSDoc comments provide value for human communication. A well-documented codebase with consistent @throws annotations helps engineers understand error paths when reading code. Documentation generation tools like TypeDoc extract @throws tags into reference pages. These benefits matter for large teams maintaining complex systems.

The disconnect between documentation and enforcement creates a specific problem: stale annotations. When a function's throw behavior changes but the JSDoc comment does not get updated, the compiler never reports the mismatch. Callers continue seeing outdated throw documentation. Tests might catch the divergence if they cover error paths, but the type system provides no safety net.

TypeScript error handling documentation approaches

TypeScript error handling documentation approaches

Some teams treat @throws annotations as required parts of their style guide and enforce them via linter rules. The eslint-plugin-jsdoc package includes a require-throws rule that demands JSDoc throw documentation on functions that contain throw statements. This catches missing documentation but does not verify that callers handle the documented exceptions.

The pattern works for teams that value documentation completeness and accept that verification happens during code review rather than compilation. For teams that need compiler-enforced error handling, different patterns become necessary.

Building Type-Safe Error Handling Without Language Support

Result types encode success and failure cases directly in return values. A function that can fail returns Result<T, E> where T represents success and E represents failure. Callers must explicitly unwrap the result to access the success value, forcing them to handle the error case.

type Result<T, E> = 
  | { ok: true; value: T }
  | { ok: false; error: E };

function parseConfig(data: unknown): Result<Config, ConfigError> {
  if (!isValidConfig(data)) {
    return { ok: false, error: new ConfigError("Invalid structure") };
  }
  return { ok: true, value: data as Config };
}

const result = parseConfig(rawData);
if (!result.ok) {
  // Compiler requires handling the error case before accessing value
  console.error(result.error);
  return;
}
// result.value is now safely typed as Config
processConfig(result.value);
Enter fullscreen mode Exit fullscreen mode

The type system enforces error handling at compile time. Attempting to access result.value without checking result.ok produces a type error. The pattern eliminates silent failure propagation because every result must be explicitly unwrapped.

Rust and Go popularized this pattern in systems programming. The Rust standard library uses Result<T, E> as its primary error handling mechanism. Go functions commonly return (value, error) tuples. These languages prove that explicit result types scale to large production systems.

TypeScript libraries provide pre-built Result type implementations. The neverthrow package offers a chainable Result API with .map(), .mapErr(), and .andThen() methods for composing operations. The ts-results package provides similar functionality with a focus on zero-dependency size. Teams can adopt these libraries or implement their own Result type based on their needs.

import { Result, ok, err } from 'neverthrow';

async function fetchUserData(id: string): Promise<Result<User, NetworkError>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return err(new NetworkError(response.status));
    }
    const data = await response.json();
    return ok(data);
  } catch (e) {
    return err(new NetworkError(0, 'Connection failed'));
  }
}

// Chaining with automatic error propagation
const result = await fetchUserData('123')
  .andThen(user => validateUser(user))
  .andThen(user => enrichUserData(user));

if (result.isErr()) {
  handleError(result.error);
} else {
  processUser(result.value);
}
Enter fullscreen mode Exit fullscreen mode

Result types add verbosity compared to exceptions. Every fallible operation returns a Result that must be unwrapped. Chaining multiple operations requires .andThen() calls or manual result checking. For teams working in exception-heavy codebases, converting to Result types represents a significant architectural shift.

The tradeoff favors type safety over brevity. Exception-based code can appear cleaner because errors propagate implicitly, but that implicit propagation creates the exact problem Result types solve. When error handling matters more than code conciseness, Result types deliver compiler-verified correctness.

Result Types vs Exceptions: The Practical Trade-Offs

Aspect Result Types Exceptions
Compile-time verification Required by type system Documentation only
Error propagation Explicit at every call Implicit through call stack
Refactoring safety Compiler catches all call sites Manual search required
Code volume Higher (wrapping overhead) Lower (implicit propagation)
Integration with existing code Requires conversion layer Works immediately
Performance overhead Negligible (return value) Negligible (modern VMs)
Team learning curve Moderate (new pattern) Minimal (familiar)

Result types force every error case to surface at the call site. When a function changes its error type from ConfigError to ConfigError | NetworkError, the compiler reports an error at every location that unwraps the result. This property makes large refactors safer because the type system identifies every affected call site automatically.

Exceptions propagate through intermediate callers without modification. A function that calls another function that might throw does not need to declare or handle the exception. The call stack unwinds until a catch block appears. This behavior reduces code volume because intermediate functions need no error-handling logic, but it also means errors can travel far from their source before getting caught.

Comparison of error propagation in Result types versus exceptions

Comparison of error propagation in Result types versus exceptions

Teams working in established codebases with thousands of exception-throwing functions face a migration challenge. Converting one function to return Result<T, E> requires updating every caller. Exceptions remain compatible with the existing architecture. For greenfield projects or isolated modules, Result types impose no integration cost.

Exception performance in modern JavaScript engines imposes negligible overhead under normal control flow. Throwing and catching exceptions costs more than returning a value, but the difference matters only in tight loops where exceptions occur frequently. Real applications rarely hit performance limits from exception handling. Result types have similarly negligible performance impact because they compile to plain object returns.

The learning curve for Result types affects teams differently based on their background. Engineers familiar with Rust, Go, or functional programming recognize the pattern immediately. Teams working exclusively in JavaScript/TypeScript need time to internalize the explicit error handling mindset. The pattern requires consistent adoption across a codebase to provide value.

Exception familiarity gives them an advantage for quick prototyping and small projects. Every JavaScript developer understands try/catch syntax. Result types require teaching the team a new pattern before they can use it effectively. For projects where time to market matters more than long-term maintainability, exceptions offer a faster path.

Error Discrimination Patterns That Actually Work in Production

Catching exceptions in TypeScript produces values typed as unknown. The type system provides no information about what gets thrown. Developers must narrow the caught value to specific error types before accessing error properties safely.

class NetworkError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
    this.name = 'NetworkError';
  }
}

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

function processRequest(data: unknown) {
  try {
    validateData(data);
    sendToServer(data);
  } catch (e) {
    if (e instanceof NetworkError) {
      console.error(`Network failed with status ${e.statusCode}`);
      return;
    }
    if (e instanceof ValidationError) {
      console.error(`Validation failed on field ${e.field}`);
      return;
    }
    // Unknown error - rethrow or log
    throw e;
  }
}
Enter fullscreen mode Exit fullscreen mode

The instanceof operator narrows the type of e within each conditional block. TypeScript understands that after e instanceof NetworkError passes, e has type NetworkError in that branch. This pattern works for class-based errors that extend Error or implement distinct prototypes.

Custom error classes should always extend Error and set a distinctive name property. This practice ensures stack traces remain accurate and error monitoring tools can distinguish error types. The built-in Error.prototype.name gets set automatically when using class syntax with extends Error.

Type guard functions provide an alternative to instanceof checks when errors do not share a common base class. A type guard returns a boolean and uses a type predicate to tell TypeScript what type the value has when the guard returns true.

interface APIError {
  code: string;
  message: string;
}

function isAPIError(e: unknown): e is APIError {
  return (
    typeof e === 'object' &&
    e !== null &&
    'code' in e &&
    'message' in e &&
    typeof (e as APIError).code === 'string'
  );
}

function handleResponse(response: unknown) {
  try {
    processResponse(response);
  } catch (e) {
    if (isAPIError(e)) {
      console.error(`API error ${e.code}: ${e.message}`);
      return;
    }
    console.error('Unknown error:', e);
  }
}
Enter fullscreen mode Exit fullscreen mode

The type guard performs runtime validation that the caught value has the expected shape. This pattern works when integrating with third-party APIs that throw plain objects rather than Error instances. The guard must check every required property because unknown values can be anything.

Error discrimination flow using instanceof and type guards

Error discrimination flow using instanceof and type guards

Error discrimination patterns in TypeScript catch blocks

Error discrimination patterns in TypeScript catch blocks

Some codebases throw string literals or plain objects instead of Error instances. These patterns work at runtime but complicate error handling. Catching a string provides no stack trace and no way to distinguish error types beyond string parsing. Modern code should always throw Error instances or subclasses.

The zod library provides a .safeParse() method that returns a discriminated union instead of throwing. This pattern bridges the gap between validation libraries that throw and Result-type error handling.

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
});

function validateUser(data: unknown) {
  const result = UserSchema.safeParse(data);
  if (!result.success) {
    // result.error is a ZodError with detailed validation failures
    console.error(result.error.format());
    return null;
  }
  // result.data is typed as { id: string; email: string }
  return result.data;
}
Enter fullscreen mode Exit fullscreen mode

The .safeParse() API demonstrates how libraries can provide both throwing and non-throwing variants. The standard .parse() method throws a ZodError on validation failure. The .safeParse() method returns { success: true; data: T } or { success: false; error: ZodError } without throwing. This gives callers the choice between exceptions and Result types per call site.

Documenting Throws Behavior for Teams and Tools

Teams that continue using exceptions need documentation strategies that scale. JSDoc @throws tags work when combined with linting rules that enforce their presence and code review processes that verify accuracy. The pattern relies on human discipline but provides value when applied consistently.

/**
 * Connects to the database with retry logic
 * @throws {ConnectionError} when all retry attempts fail
 * @throws {AuthenticationError} when credentials are invalid
 * @throws {TimeoutError} when connection exceeds deadline
 */
async function connectDatabase(config: DatabaseConfig): Promise<Connection> {
  // implementation
}
Enter fullscreen mode Exit fullscreen mode

The eslint-plugin-jsdoc package includes rules that enforce @throws documentation. The require-throws rule checks that functions containing throw statements have corresponding @throws tags. Configuration options control whether the rule applies to all functions or only exported ones. Related tooling context shows how modern linters handle JSDoc validation.

Code review checklists should include verification that @throws tags match actual throw behavior. When a pull request modifies a function's error handling, reviewers check that JSDoc comments get updated accordingly. This manual process catches most documentation drift but depends on reviewer attention.

Documentation flow for throw behavior in team workflows

Documentation flow for throw behavior in team workflows

TypeDoc and similar documentation generators parse @throws tags into generated reference documentation. The official TypeDoc documentation (accessed via npm package typedoc version 0.26.x) shows that @throws tags appear in a dedicated "Throws" section of function documentation. This makes error behavior visible in generated API docs but does not affect compilation.

Some teams encode error types in function names as a lightweight alternative to comprehensive JSDoc. Functions named fetchUserOrThrow signal explicitly that they throw on failure, while tryFetchUser suggests they return null or undefined. This convention provides no type safety but improves code readability.

Error boundaries in React applications demonstrate a practical exception-based pattern that works without type system support. A component wraps children and catches errors that propagate during rendering. Related distributed context explores how error tracking spans service boundaries.

class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error?: Error }
> {
  state = { hasError: false, error: undefined };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}
Enter fullscreen mode Exit fullscreen mode

Error boundaries rely on React's lifecycle methods rather than TypeScript's type system. The pattern works because React guarantees when componentDidCatch gets called, eliminating the need for static analysis to track throw behavior through the component tree.

Frequently Asked Questions

Why doesn't TypeScript just add a throws keyword like Java?

JavaScript has unchecked exceptions and no runtime enforcement of throw declarations. TypeScript follows JavaScript semantics and avoids features that require fundamental runtime changes. The complexity of accurately modeling JavaScript's dynamic throw behavior through function references, callbacks, and async operations exceeds the benefit.

Can I use a compiler plugin to add throw type checking?

No compiler plugin system exists in TypeScript that allows custom type checking logic. The TypeScript compiler architecture does not support plugins that extend the type checker. Teams must work within the type system's built-in capabilities or use external tools like custom linters.

Do Result types work with async/await?

Result types work with async/await when the Promise resolves to a Result. Functions return Promise<Result<T, E>> instead of throwing. Callers await the Promise and then unwrap the Result with the same patterns used for synchronous code. The neverthrow library provides .asyncMap() and similar utilities for async Result composition.

How do I convert an exception-throwing library to use Result types?

Wrap library calls in adapter functions that catch exceptions and return Results. The adapter provides a typed boundary between exception-based external code and Result-based internal code. This pattern contains the exception handling at the integration point without requiring changes to the library itself.

What happens to stack traces when using Result types?

Result types preserve stack traces when the error case contains an Error instance. Creating errors with new Error() captures the call stack at that point. Result types simply return the error rather than throwing it, so the stack trace remains intact and accessible through the error's .stack property.

What to Do Until (If Ever) TypeScript Gets Throw Types

TypeScript will likely never support throw types given the language's design constraints and JavaScript's runtime semantics. The core team has not signaled any movement toward this feature in recent releases. Teams building production TypeScript applications must accept that exception typing will remain outside the type system's guarantees.

Result types provide the strongest alternative for codebases that need verifiable error handling. The pattern requires architectural commitment and training investment, but it delivers compile-time verification that exceptions cannot match. New projects should consider Result types from the start when error handling matters more than code brevity.

Exception-based codebases should combine @throws JSDoc with linter enforcement and code review processes. This approach lacks type system verification but provides human-readable documentation that scales to large teams. The pattern works when the team values familiar exception syntax and accepts manual verification during review.

Error discrimination using instanceof checks and type guards makes exception handling safer when exceptions remain the chosen approach. Every catch block should narrow the caught value to specific error types before accessing properties. This pattern prevents runtime errors from assumed error shapes while working within TypeScript's type system.

The library architecture patterns that underpin maintainable TypeScript projects apply equally to error handling strategy. Choose one approach and apply it consistently across the codebase rather than mixing patterns that create confusion at boundaries. The consistency matters more than the specific choice between Results and exceptions for most applications.

Top comments (0)