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 toward more resilient and explicit error management.
The Pitfalls of Overlooking Error Handling
Consider this TypeScript division function:
const const divide: (a: number, b: number) => number
divide = (a: number
a: number, b: number
b: number) => a: number
a / b: number
b;
This function appears straightforward but fails when b
is zero, returning Infinity
. Such overlooked cases can lead to illogical outcomes:
const const calculateAverageSpeed: (distance: number, time: number) => string
calculateAverageSpeed = (distance: number
distance: number, time: number
time: number) => {
const const averageSpeed: number
averageSpeed = const divide: (a: number, b: number) => number
divide(distance: number
distance, time: number
time);
return `${const averageSpeed: number
averageSpeed} km/h`;
};
// will be "Infinity km/h"
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.log("Average Speed: ", const calculateAverageSpeed: (distance: number, time: number) => string
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:
type type Success<T> = {
kind: "success";
value: T;
}
Success<function (type parameter) T in type Success<T>
T> = { kind: "success"
kind: "success"; value: T
value: function (type parameter) T in type Success<T>
T };
type type Failure<E> = {
kind: "failure";
error: E;
}
Failure<function (type parameter) E in type Failure<E>
E> = { kind: "failure"
kind: "failure"; error: E
error: function (type parameter) E in type Failure<E>
E };
type type Result<T, E> = Success<T> | Failure<E>
Result<function (type parameter) T in type Result<T, E>
T, function (type parameter) E in type Result<T, E>
E> = type Success<T> = {
kind: "success";
value: T;
}
Success<function (type parameter) T in type Result<T, E>
T> | type Failure<E> = {
kind: "failure";
error: E;
}
Failure<function (type parameter) E in type Result<T, E>
E>;
function function divide(a: number, b: number): Result<number, string>
divide(a: number
a: number, b: number
b: number): type Result<T, E> = Success<T> | Failure<E>
Result<number, string> {
if (b: number
b === 0) {
return { kind: "failure"
kind: "failure", error: string
error: "Cannot divide by zero" };
}
return { kind: "success"
kind: "success", value: number
value: a: number
a / b: number
b };
}
Handling the Result in TypeScript
const const handleDivision: (result: Result<number, string>) => void
handleDivision = (result: Result<number, string>
result: type Result<T, E> = Success<T> | Failure<E>
Result<number, string>) => {
if (result: Result<number, string>
result.kind: "success" | "failure"
kind === "success") {
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args) for more information.log("Division result:", result: Success<number>
result.value: number
value);
} else {
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v22.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v22.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stderr` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)).
```js
const code = 5;
console.error('error #%d', code);
// Prints: error #5, to stderr
console.error('error', code);
// Prints: error 5, to stderr
```
If formatting elements (e.g. `%d`) are not found in the first string then
[`util.inspect()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilinspectobject-options) is called on each argument and the
resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v22.x/api/util.html#utilformatformat-args)
for more information.error("Division error:", result: Failure<string>
result.error: string
error);
}
};
const const result: Result<number, string>
result = function divide(a: number, b: number): Result<number, string>
divide(10, 0);
const handleDivision: (result: Result<number, string>) => void
handleDivision(const result: Result<number, string>
result);
Native Rust Implementation for Comparison
In Rust, the Result
type is an enum with variants for success and error:
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?
- Explicit Handling: Necessitates handling both outcomes, enhancing code robustness.
- Clarity: The code’s intention becomes more apparent.
- Safety: It reduces the chances of uncaught exceptions.
- Functional Approach: Aligns with TypeScript’s functional programming style.
Leveraging ts-results for Rust-Like Error Handling
For TypeScript developers, the 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.