Navigating the Murky Waters of JavaScript Exceptions: A TypeScript Perspective
JavaScript Exceptions and TypeScript
JavaScript's runtime error model is one of its most persistent pain points. There is no throws declaration, no checked exceptions, and no compile-time contract that tells you which functions can fail and under what conditions. Errors surface at runtime, often far from their origin, and with little indication of what went wrong.
What JavaScript Does Not Tell You#
In a statically typed language like Java, the throws keyword is a contract: this method may produce this exception type, and callers must handle it. JavaScript has no such mechanism. Any function can throw at any time. Any property access on an unexpected null or undefined will fail. Any network response can return something entirely different from what the type signature says.
Consider a function that processes data from an external API:
function processUser(user) {
return user.name.toUpperCase();
}
If user is null, user.name is undefined, or the data does not match expectations, this throws at runtime. Nothing in the code signals that this function is fragile. The failure mode is invisible until it happens.
What TypeScript Adds#
TypeScript introduces static typing and compile-time checking. It cannot eliminate all runtime errors, but it can catch a significant class of type-related mistakes before they reach production.
interface User {
name: string;
age: number;
}
function processUser(user: User): string {
return user.name.toUpperCase();
}
processUser(null); // Compile error: Argument of type 'null' is not assignable to parameter of type 'User'
processUser({ age: 30 }); // Compile error: Property 'name' is missing
TypeScript enforces the contract at the call site. Bad inputs produce a compile-time error rather than a runtime crash.
With strict null checks enabled, TypeScript also prevents unsafe property access:
function processUser(user: User | null): string {
return user.name.toUpperCase(); // Error: Object is possibly 'null'
}
function processUser(user: User | null): string {
if (user === null) return "";
return user.name.toUpperCase(); // Safe
}
The Boundary TypeScript Cannot Protect#
TypeScript's types are erased at runtime. When data arrives from an external source—an HTTP response, a form submission, a configuration file—TypeScript has no way to verify that it matches the declared type. The runtime simply trusts the annotation.
interface User {
name: string;
age: number;
}
const response = await fetch('/api/user');
const user: User = await response.json(); // TypeScript trusts this completely
console.log(user.name.toUpperCase()); // Runtime error if API returns unexpected shape
The annotation provides type safety within the application, but nothing validates the data at the point it enters from outside. This is where TypeScript's guarantees end.
Addressing this requires runtime validation—libraries like Zod or Yup that check the actual shape of incoming data against a schema before it is used.
Handling Exceptions in Practice#
A few practices reduce the surface area of runtime errors in JavaScript and TypeScript:
Try-catch at boundaries: Wrap external calls—network requests, file reads, parsing—in try-catch blocks. Handle failures explicitly rather than letting them propagate unchecked.
Validate external data: Do not annotate API responses with an interface and move on. Validate the structure at runtime before treating the data as trusted.
Enable strict TypeScript:
"strict": trueintsconfig.jsonenables null checks, strict function types, and other checks that catch common error patterns at compile time.Explicit error handling over silent failures: Avoid returning
nullorundefinedfrom functions that can fail. Return a discriminated union ({ success: true, data: T } | { success: false, error: string }) so callers are forced to handle both paths.
Conclusion#
TypeScript brings meaningful structure to JavaScript's error landscape. It catches type mismatches and null safety issues at compile time, providing confidence that correctly typed code will not fail for type-related reasons. But TypeScript's safety stops at the application boundary. Runtime validation is required to handle untrusted external data safely.
The two tools are complementary: TypeScript for internal correctness, runtime validation for external correctness.