Robust Error Handling in TypeScript: A Journey from Naive to Rust-Inspired Solutions
Tags: webdev, beginners, typescript
Published: 2023-11-18 18:29:38

## Introduction

In the dynamic world of software development, robust error handling isn't just best practice; it's essential for reliable software. Well-written code may face unexpected challenges, particularly in production. As developers, preparing our applications to gracefully handle these uncertainties is crucial. This post explores enhancing TypeScript error handling, inspired by Rust's Result pattern—a shift towards more resilient and explicit error management.

## The Pitfalls of Overlooking Error Handling

Consider this TypeScript division function:

```typescript
const divide = (a: number, b: number) => a / b;
```

This function appears straightforward but fails when `b` is zero, returning `Infinity`. Such overlooked cases can lead to illogical outcomes:

```typescript
const calculateAverageSpeed = (distance: number, time: number) => {
    const averageSpeed = divide(distance, time);
    return `${averageSpeed} km/h`;
};

// will be "Infinity km/h"
console.log("Average Speed: ", calculateAverageSpeed(50, 0)); 
```

## Embracing Explicit Error Handling

TypeScript offers various error management techniques. Adopting a more explicit approach, inspired by Rust, can enhance code safety and predictability.

### Result Type Pattern: A Rust-Inspired Approach in TypeScript

Rust is known for its explicit error handling through the `Result` type. Let's mirror this in TypeScript:

```typescript
type Success<T> = { kind: 'success', value: T };
type Failure<E> = { kind: 'failure', error: E };
type Result<T, E> = Success<T> | Failure<E>;

function divide(a: number, b: number): Result<number, string> {
    if (b === 0) {
        return { kind: 'failure', error: 'Cannot divide by zero' };
    }
    return { kind: 'success', value: a / b };
}
```

### Handling the Result in TypeScript

```typescript
const handleDivision = (result: Result<number, string>) => {
    if (result.kind === 'success') {
        console.log("Division result:", result.value);
    } else {
        console.error("Division error:", result.error);
    }
};

const result = divide(10, 0);
handleDivision(result); // "Division error: Cannot divide by zero"
```

### Native Rust Implementation for Comparison

In Rust, the `Result` type is an enum with variants for success and error:

```rust

fn divide(a: i32, b: i32) -> std::result::Result<i32, String> {
    if b == 0 {
        std::result::Result::Err("Cannot divide by zero".to_string())
    } else {
        std::result::Result::Ok(a / b)
    }
}

fn main() {
    match divide(10, 2) {
        std::result::Result::Ok(result) => println!("Division result: {}", result),
        std::result::Result::Err(error) => println!("Error: {}", error),
    }
}

```

### Why the Rust Way?

1. **Explicit Handling**: Necessitates handling both outcomes, enhancing code robustness.
2. **Clarity**: The code's intention becomes more apparent.
3. **Safety**: It reduces the chances of uncaught exceptions.
4. **Functional Approach**: Aligns with TypeScript's functional programming style.

## Leveraging ts-results for Rust-Like Error Handling

For TypeScript developers, the [ts-results](https://github.com/vultix/ts-results) library is a great tool to apply Rust's error handling pattern, simplifying the implementation of Rust’s `Result’ type in TypeScript.

## Conclusion

Adopting Rust's `Result’ pattern in TypeScript, with tools like ts-results, significantly enhances error handling strategies. This approach effectively tackles errors while upholding the integrity and usability of applications, transforming them from functional to resilient.

Let's embrace these robust practices to craft software that withstands the tests of time and uncertainty.