Created
February 16, 2023 16:58
-
-
Save baptistemanson/dfb1673ef4d34ea72aa076aaab8ff1bf to your computer and use it in GitHub Desktop.
An example of how error handling could look like in TS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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