Error Handling in TypeScript

Learn about Error Handling in TypeScript with this detailed tutorial. Includes explanations and examples to help you master error handling.

Introduction

Error handling in TypeScript is crucial for building robust applications. TypeScript extends JavaScript's error handling capabilities with its type system, enabling you to catch errors early during development and ensuring type safety.

Basic Error Handling

TypeScript uses the same try-catch-finally mechanism as JavaScript for error handling.

function riskyFunction() {
    throw new Error("Something went wrong");
}

try {
    riskyFunction();
} catch (error) {
    console.error("Caught an error:", error.message);
} finally {
    console.log("Cleaning up...");
}

Explanation

In this example, the riskyFunction throws an error, which is then caught in the try block. The catch block handles the error, and the finally block executes cleanup code.

Typed Errors

You can create custom error classes to handle different types of errors in a more structured way.

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

function riskyFunction() {
    throw new CustomError("Custom error occurred");
}

try {
    riskyFunction();
} catch (error) {
    if (error instanceof CustomError) {
        console.error("Caught a custom error:", error.message);
    } else {
        console.error("Caught an unknown error:", error);
    }
}

Explanation

In this example, a custom error class CustomError is created. The riskyFunction throws an instance of this custom error, which is then handled in the catch block using the instanceof operator.

Asynchronous Error Handling

Handling errors in asynchronous code can be done using try-catch within async functions or by attaching .catch handlers to promises.

async function asyncFunction() {
    throw new Error("Async error");
}

async function run() {
    try {
        await asyncFunction();
    } catch (error) {
        console.error("Caught an async error:", error.message);
    }
}

run();

Explanation

In this example, the asyncFunction throws an error, which is caught using a try-catch block within another async function run.

Error Handling with Fetch

Handling errors during HTTP requests can be done using try-catch with async/await or .catch with promises.

async function fetchData(url: string) {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error("Error fetching data:", error.message);
        throw error;
    }
}

fetchData("https://api.example.com/data")
    .then(data => console.log("Data:", data))
    .catch(error => console.error("Caught in .catch:", error.message));

Explanation

In this example, the fetchData function fetches data from a URL and handles errors using try-catch. It also demonstrates how to handle errors using the .catch method on the promise.

Strict Null Checks

TypeScript's strict null checks help prevent runtime errors by ensuring that values are properly checked before being used.

function getLength(str: string | null): number {
    if (str === null) {
        throw new Error("String is null");
    }
    return str.length;
}

try {
    console.log(getLength("Hello")); // 5
    console.log(getLength(null)); // Throws error
} catch (error) {
    console.error("Caught an error:", error.message);
}

Explanation

In this example, the getLength function checks if the input string is null and throws an error if it is. This helps prevent null reference errors at runtime.

Using Optionals and Default Values

You can handle potential undefined values by using optional chaining and default values.

interface User {
    name: string;
    age?: number;
}

function printUserInfo(user: User) {
    const age = user.age ?? "Unknown age";
    console.log(`Name: ${user.name}, Age: ${age}`);
}

printUserInfo({ name: "Alice", age: 25 }); // Name: Alice, Age: 25
printUserInfo({ name: "Bob" }); // Name: Bob, Age: Unknown age

Explanation

In this example, the printUserInfo function uses the nullish coalescing operator (??) to provide a default value for the age property if it is undefined.

Custom Error Handling Logic

You can create custom error handling logic to provide more detailed and specific error messages.

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

function validateUser(user: User) {
    if (!user.name) {
        throw new ValidationError("Name is required");
    }
    if (user.age && user.age < 0) {
        throw new ValidationError("Age must be a positive number");
    }
}

try {
    validateUser({ name: "", age: -1 });
} catch (error) {
    if (error instanceof ValidationError) {
        console.error("Validation Error:", error.message);
    } else {
        console.error("Unknown Error:", error);
    }
}

Explanation

In this example, the ValidationError class provides a custom error type. The validateUser function uses this custom error to provide specific error messages for validation failures.

Conclusion

Error handling in TypeScript extends JavaScript's capabilities with its type system, enabling you to catch errors early and ensure type safety. By understanding and using these error handling techniques, you can write more robust and maintainable code.