throw new Error ('Houston, we have a problem')
const divide = (param1: number, param2: number): number => {
if (param2 === 0)
throw new Error("Attempt to divide with zero");
return param1 / param2;
};
try {
const result = divide(a, b)
return `result: ${result}`
} catch (e) { // optional if finally exist
// Code to run if an exception occurs
} finally { // optional if catch exist
// Code that is always executed
}
- the type of a function says nothing if it can throw an exception.
- difficult to compose the function into a larger computation.
Exception-oriented code
- Introducing alternative futures (side effects).
- Not type-safe 🤖
const divide = (
param1: number,
param2: number
): number | NaN => {
if (param2 === 0)
return NaN;
return param1 / param2;
};
const result = divide(a, b)
if (Number.isNaN(result)) {
// console.log(`Result: ${result}`)
} else {
// Run code for special case
}
- No specific semantic meaning 🙃
- Creates need for boilerplate error checking code 🤔
- Forces a special policy on callers, makes it difficult to compose with other functions ⚠️
const divide = (
param1: number,
param2: number
): Option<number> => {
if (param2 === 0)
return none;
return some(param1 / param2)
};
const result = divide(1/2)
if (result instanceof Some) {
// do something!
console.log(result.value)
} else if (result instanceof NONE) {
// Specific error
} else {
// Another kind of error
}
how does this work? ⬇️
Factoring out common error-handling patterns into higher-order functions
export class None<A>
extends OptionBase<A> {
// `never` is the "bottom type"
// `static` creates "class" property
static readonly NONE:
Option<never> = new None();
readonly value: "none" = "none";
// `private` prevents access
// by external code
private constructor() {
super();
}
}
export type Option<A> = Some<A> | None<A>;
export const none = <A>(): Option<A> => None.NONE;
export const some = <A>(a: A): Option<A> => new Some(a);
export class Some<A>
extends OptionBase<A> {
readonly value: "some" = "some";
// 6. classes must call `super()`
// if they extend other classes
constructor(value: A) {
this.value = value
super();
}
}
Example here ▶️
const customError = () => ({
message: `A number cannot be divided by zero`,
})
const divide = (
param1: number,
param2: number
): Either<{ message: string }, number> => {
if (param2 === 0)
return left(customError())
return right(param1 / param2)
};
const result = divide(a, b)
if (result.isLeft()) {
const { message } = result.value;
// Code to run if an exception occurs
} else {
return `result: ${result.value}`
}
how does this work? ⬇️
export class Left<L, A> {
readonly value: L;
constructor(value: L) {
this.value = value;
}
isLeft(): this is Left<L, A> {
return true;
}
isRight(): this is Right<L, A> {
return false;
}
}
export class Right<L, A> {
readonly value: A;
constructor(value: A) {
this.value = value;
}
isLeft(): this is Left<L, A> {
return false;
}
isRight(): this is Right<L, A> {
return true;
}
}
export type Either<L, A> = Left<L, A> | Right<L, A>;
export const left = <L, A>(l: L): Either<L, A> => {
return new Left(l);
};
export const right = <L, A>(a: A): Either<L, A> => {
return new Right<L, A>(a);
};
export type Either<L, A> = Left<L, A> | Right<L, A>;
export class Left<L, A> {
// ......
applyOnRight<B>(_: (a: A) => B): Either<L, B> {
return this as any;
}
}
export class Right<L, A> {
// ......
applyOnRight<B>(func: (a: A) => B): Either<L, B> {
return new Right(func(this.value));
}
}
// ......
const result = divide(1, 2);
return result.applyOnRight(doSomething);
Common pattern: If there's an error do nothing, else do something...
Creating a chain of operations 🔗
➡️
- TypeScript Deep Dive: Exception Handling
- Functional Programming in TypeScript: Handling errors
- Bruno Vegreville: Expressive error handling in TypeScript
- ts-fp: Option data type
- Programming TypeScript: Making Your JavaScript Applications Scale