Skip to content

Instantly share code, notes, and snippets.

@baptistemanson
Created February 16, 2023 16:58
Show Gist options
  • Save baptistemanson/dfb1673ef4d34ea72aa076aaab8ff1bf to your computer and use it in GitHub Desktop.
Save baptistemanson/dfb1673ef4d34ea72aa076aaab8ff1bf to your computer and use it in GitHub Desktop.
An example of how error handling could look like in TS
// Better error handling in TS
// Exceptions in TS have a few issues:
// - they can't be typechecked
// - they can't be easily documented (this may fail)
// - the "catch" code is usually not colocated to what may fail, adding cognitive load
// - it's hard to differentiate between different errors
// - they are costly in performance
// - they can propagate all the way from the bottom to the top without the intermediate layers to be warned it may fail
// - anything can be thrown
// A second problem this can tackle is nullable thing, using the Option type.
// Here is a first proposal modelled after Go and Rust.
// we support string and things that have error codes.
// error codes are great for callers to differentiate errors
// but also because we can add metadata to understand the error.
type IdeaError = string | { code: string };
type Result<V, E extends IdeaError> =
| { ok: true; value: V }
| { ok: false; error: E };
// utility functions to generate results
function ok<V>(value: V): { ok: true; value: V } {
return { ok: true, value };
}
function error<E>(error: E): { ok: false; error: E } {
return { ok: false, error };
}
type ExampleError = "e1" | "e2";
// an example of how it helps documenting a function
const basic = (): Result<number, ExampleError> => {
if (1) {
return error("lol"); // doesnt typecheck as expected
}
if (2) {
return error("e1");
}
return ok(10);
};
// an example on how it helps enforcing handling errors
// and ease figuring out which error occured
const handleExample = (): number => {
const r = basic();
if (r.ok) {
// safe way to access
return r.value;
} else if (r.error == "e1") {
return -1;
}
return -2;
};
// will propagate to the caller of bubbleUp
const bubbleUpExample = (): Result<string, ExampleError> => {
const r = basic();
if (!r.ok) {
return r;
}
return ok(String(r.value));
};
// falls back to the exception world,
// for progressive improvement of the code base
const bubbleTopExample = (): number => {
return unwrap(basic());
};
// another way to declare error interface
enum MergeError {
E3 = "e3",
E4 = "e4",
}
// an example on how to merge 2 types of errors
const mergeExample = (): Result<string, ExampleError | MergeError> => {
const r = basic();
if (!r.ok) {
return r;
}
return error(MergeError.E3);
};
// an example on how to convert a lower layer to result based behavior
const mayThrow = () => {
throw new Error(MergeError.E3);
};
const wrapExample = (): Result<string, MergeError> => {
const r = wrap<any, MergeError>(() => mayThrow());
if (!r.ok) {
return r;
}
return ok("go");
};
// implementation details
// utility function to get the value inside a result
// similar to !, but we can find them easily
// we have clear error messages
// they should have a proper stack trace starting at the second frame
function unwrap<V, E extends IdeaError>(r: Result<V, E>): V {
if (r.ok) return r.value;
else {
const errorMessage = typeof r.error === "string" ? r.error : r.error.code;
throw new Error(errorMessage);
}
}
function wrap<T, E extends IdeaError>(callable: () => T): Result<T, E> {
try {
return ok(callable());
} catch (thrown: unknown) {
let errMessage = wrap.caller.name;
if (typeof thrown === "string") {
errMessage = thrown;
} else if (
typeof thrown === "object" &&
thrown !== null &&
thrown.hasOwnProperty("toString")
) {
errMessage = thrown.toString();
}
return error(errMessage) as { ok: false; error: E };
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment