For DevelopersJune 09, 2026

Throwing Exceptions in TypeScript: A Beginner's Guide (2026)

To throw an exception in TypeScript, use the throw statement with a new Error() and catch it in a try-catch block. Always throw Error instances for stack traces, and narrow the catch variable, which is typed as unknown in modern strict TypeScript, before reading its message.

5 key takeaways

  • Throw Error instances, not strings or plain objects. Only Error objects carry a stack trace and work with instanceof checks.
  • The catch variable is unknown in modern TypeScript. Since version 4.4 under strict mode, you must narrow with error instanceof Error before reading .message.
  • Custom error classes add context. A named class such as InvalidUserInputError is easier to catch and route than a generic Error.
  • Reserve exceptions for the unexpected. For expected, recoverable failures, a Result type from a library like neverthrow makes failure explicit in the type signature.
  • finally always runs. Use it for cleanup such as closing connections or releasing locks, whether or not an error was thrown.

Introduction to exception handling in TypeScript

Exception handling is a fundamental part of programming. When something unexpected happens, such as incorrect user input or an unreachable network resource, you need a way to detect the problem and respond to it without crashing the whole program.

Throwing exceptions is the standard way to signal and handle unexpected events in TypeScript. In this beginner tutorial, we cover the basics of throwing exceptions, look at built-in and custom error types, and walk through the practices that hold up in production code. TypeScript handles exceptions the same way JavaScript does, with the throw statement and try-catch blocks. What sets TypeScript apart is type safety, which helps you catch many error-prone paths during development, before the code ever runs.

Unhandled code versus handled code: a before and after comparison of error handling in TypeScript

Unhandled code fails loudly and silently in equal measure. Handled code recovers, reports a clear typed error, and stays easy to debug.

Understanding the throw statement

The throw statement raises an exception. It stops execution in the current block and transfers control to the next error handler, which is usually a catch block.

function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error("Division by zero is not allowed");
  }
  return a / b;
}

try {
  console.log(divide(10, 0));
} catch (error) {
  console.error("Error:", error instanceof Error ? error.message : String(error));
}

If b is zero, an exception is raised with the message "Division by zero is not allowed". The throw moves control to the catch block, where the error is reported. Notice the narrowing with error instanceof Error. We explain why that matters in the section on the unknown catch variable below.

Types of values you can throw

You can throw a range of values, including:

  • Strings, for example throw "An error occurred";
  • Objects, for example throw { error: "Custom Error" };
  • Error instances, for example throw new Error("Standard Error");

In general it is best to throw Error instances. They carry a stack trace and other useful debugging information, and they play nicely with instanceof checks. A thrown string or plain object gives you none of that.

Using built-in error types in TypeScript

JavaScript ships with built-in error types, and they are available in TypeScript too. The common ones are:

  • Error, the base error type, used for generic problems.
  • TypeError, raised when an operation runs on the wrong type.
  • SyntaxError, thrown on a syntax problem, such as a bad JSON.parse.
  • RangeError, used when a value sits outside an allowed range, such as a number past an intended limit.
function getItem(items: unknown[], index: number) {
  if (index >= items.length || index < 0) {
    throw new RangeError("Index out of range");
  }
  return items[index];
}

try {
  console.log(getItem([1, 2, 3], 5));
} catch (error) {
  if (error instanceof RangeError) {
    console.error(error.message); // Output: Index out of range
  }
}

A RangeError is raised when the index falls outside the array boundaries. Using a specific error type makes the code easier to read and to debug.

Adding custom errors in TypeScript

Sometimes the built-in error types do not fully describe the problem. In those cases a custom error class is useful. Custom errors add context. A specific InvalidUserInputError reads more clearly than a generic Error, and it lets a caller catch exactly that failure.

class InvalidUserInputError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "InvalidUserInputError";
  }
}

function getUserAge(age: number): number {
  if (age < 0) {
    throw new InvalidUserInputError("Age cannot be negative");
  }
  return age;
}

try {
  console.log(getUserAge(-5));
} catch (error) {
  if (error instanceof InvalidUserInputError) {
    console.error("User input error:", error.message);
  }
}

InvalidUserInputError extends the base Error class and sets a unique name, which makes it easy to identify in a catch block. For production code, also set Object.setPrototypeOf(this, InvalidUserInputError.prototype) if you target ES5, so that instanceof keeps working after transpilation.

Error handling using try-catch in TypeScript

The try-catch block lets you capture and handle errors without crashing the program. The optional finally block runs whether or not an error was thrown, which makes it the right place for cleanup such as closing a file or releasing a lock.

try {
  // Code that might throw an error
} catch (error) {
  // Handle the error here
} finally {
  // Code that runs whether or not an error was thrown
}
How try, catch, and finally run: control jumps from throw straight to the matching catch block, then finally always runs

The execution path: code runs inside try, a throw raises an Error, the matching catch handles it, and finally always runs at the end.

Here is a practical example that guards against malformed input:

function parseJson(json: string): unknown {
  try {
    return JSON.parse(json);
  } catch (error) {
    console.error("Invalid JSON format:", error instanceof Error ? error.message : error);
    return null;
  }
}

parseJson("invalid json"); // Output: Invalid JSON format: Unexpected token ...

The unknown catch variable in modern TypeScript

This is the one detail most beginner tutorials still get wrong. Since TypeScript 4.4, with useUnknownInCatchVariables on (it is included in strict mode), the variable in a catch clause is typed as unknown, not any. That means error.message does not compile on its own, because anything can be thrown, including a string or a number.

The fix is to narrow the value before you read from it:

try {
  riskyCall();
} catch (error) {
  // In TypeScript 4.4+ with strict settings, `error` is typed as `unknown`.
  // You must narrow it before reading `.message`.
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("Unknown error:", error);
  }
}

This is a real improvement over older guides that read error.message directly. The compiler now forces you to handle the case where the thrown value is not an Error at all. Every code sample in this article follows that pattern.

Best practices for throwing exceptions

  • Do not overuse exceptions. Reserve them for genuinely unexpected situations. A failed login attempt, for example, is an expected outcome and should return a result, not throw.
  • Be specific with custom errors. Named error types improve clarity and make failures easy to catch and route.
  • Write clear, informative messages. Say what went wrong and why, so the next person to read the log can act on it.
  • Always throw Error instances. They preserve the stack trace and work with instanceof.

Common pitfalls in exception handling

  • Throwing for non-exceptional cases. If a status check can return a default value, return it instead of throwing.
  • Overusing catch blocks. A catch on every call hides the real control flow. Only catch where you can actually do something about the error.
  • Generic messages. Avoid "Something went wrong". Describe what triggered the exception as precisely as you can.
  • Swallowing errors. An empty catch block hides failures. At minimum, log the error or rethrow it.

Advanced techniques: type narrowing and async

TypeScript gives you type guards and union types to handle more sophisticated error flows. Type guards narrow the kind of error inside a catch block so you can branch on it safely.

try {
  throw new InvalidUserInputError("Invalid age");
} catch (error) {
  if (error instanceof InvalidUserInputError) {
    console.error("Custom error:", error.message);
  } else if (error instanceof Error) {
    console.error("General error:", error.message);
  } else {
    console.error("Non-error thrown:", error);
  }
}

In async functions, errors should be handled with try-catch as well. A throw inside an async function rejects the returned promise, so an unhandled one surfaces as an unhandledrejection event. Always wrap awaited calls that can fail, and check response.ok on a fetch, because fetch does not throw on HTTP error status codes by itself.

async function fetchData(url: string): Promise<unknown> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Request failed with status ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("Failed to fetch data:", error instanceof Error ? error.message : error);
    return null;
  }
}

When not to throw: exceptions versus result types

Throwing is not the only way to model failure, and for expected errors it is often not the best one. A growing pattern in TypeScript codebases is the Result type, where a function returns either a success value or an error value instead of throwing. The caller then has to handle both cases, and the compiler enforces it.

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

function safeDivide(a: number, b: number): Result<number> {
  if (b === 0) {
    return { ok: false, error: new Error("Division by zero is not allowed") };
  }
  return { ok: true, value: a / b };
}

const result = safeDivide(10, 0);
if (result.ok) {
  console.log(result.value);
} else {
  console.error(result.error.message); // handled, no throw needed
}

Libraries such as neverthrow and Effect formalize this pattern with a typed Result or Either. The trade-off is straightforward: throwing is concise and bubbles up automatically, but it is invisible in the type signature, so a caller cannot tell from the types that a function might fail. A Result makes failure explicit and impossible to ignore, at the cost of more verbose call sites.

ScenarioThrow an exceptionReturn a Result
Programmer error or impossible stateYesNo
Expected, recoverable failure (bad input, not found)NoYes
Deep call stack, error handled far awayYesVerbose
Failure must be visible in the type signatureNoYes
Library boundary, callers in other languagesYesNo

A useful rule of thumb: throw for the unexpected, return a Result for the expected. Most production TypeScript ends up using both.

Conclusion

In this guide we covered the fundamentals of throwing and handling exceptions in TypeScript: the throw statement, built-in and custom error types, try-catch-finally, the unknown catch variable in modern TypeScript, type narrowing, async error handling, and when to reach for a Result type instead. Exception handling is critical for building robust applications, and TypeScript's type safety makes it more powerful than plain JavaScript. Specific error types, custom error classes, and the practices above will help you write cleaner, more readable, and more reliable code. Try the samples above to see how solid error handling improves your projects.

For developers

Ready to put these TypeScript skills to work? Index.dev is an AI-first engineering talent platform that connects companies with the top 1% of senior engineers from LATAM and CEE, around 30,000 developers, each human-vetted through technical and live interviews from a pool of 2.5M+. Join Index.dev for high-paying remote projects with global companies and boost your career today.

For clients

Need expert TypeScript developers? Hire from Index.dev's network of human-vetted senior engineers from LATAM and CEE, matched to your stack in 48 hours, with 40 to 60 percent cost savings on engineering projects. Only about 1.2 percent of applicants are accepted, and 97 percent of clients return for a second engagement. Start hiring now.

Share

Ali MojaharAli MojaharSEO Specialist

Related Articles

For DevelopersHow to Check the Type of Variable in Python (+Examples)
Python
Use type() when you need the exact class, and isinstance() when subclasses should count. For real safety, do not lean on runtime checks. Add type hints with built-in generics like list[int], then catch errors before they ship with a static checker such as mypy, Pyright, or ty.
Ali MojaharAli MojaharSEO Specialist
For EmployersDeepSeek vs. ChatGPT in 2026: Which AI Model Wins for Your Team?
Software DevelopmentArtificial Intelligence
A 2026 head-to-head of DeepSeek and ChatGPT across performance, cost, user experience, and production fit, with current benchmarks and a decision matrix for engineering teams.
Alina PohilencoAlina PohilencoData Manager