An Interest In:
Web News this Week
- April 27, 2024
- April 26, 2024
- April 25, 2024
- April 24, 2024
- April 23, 2024
- April 22, 2024
- April 21, 2024
goodbye try/catch hello error-return
Want your code to never throw errors during runtime? You can get pretty close using this error-return pattern inspired by golang. (I assume Go copied it from a long tradition.)
The problem: Typescript and javascript have no way to indicate that a function may throw an error. So you typically have to either run your code until it crashes or hunt down the source code in github or node_modules (since the bundled dist usually only has non-minified headers) to figure out where to try/catch.
The error-return pattern makes your tooling track where errors may occur so you don't have to memorize or hunt for that information yourself. With error-return, an unhandled error is immediately shown in your editor with a typescript error.
Here's an example of the pattern in a sequential, branching networking task:
async function loadSong(id: string): Err | Song { const metadata = await loadMetadata(id) if (isErr(metadata)) return metadata // returns error! const mp3 = await loadMp3(id) if (!isErr(mp3)) return Song(metadata, mp3) // try ogg it might work const ogg = await loadOgg(id) if (!isErr(ogg)) return Song(metadata, ogg) // maybe the mirror has it? const mirrorMp3 = await loadMp3(id, { useMirror: true }) if (!isErr(mirrorMp3)) return Song(metadata, mirrorMp3) return Err('all audio hosts failed')}
(Quite nice compared to four levels of indentation with try-catch.)
Then, if you tried to use this function without catching the error in, say, an html element you would get a type error:
const addElm = document.body.appendChildfunction playSong(id: string) { const song = loadSong(id) addElm(Player(song).play()) // typescript error: .play() does not exist on type Err addElm(Metadata(song)) // same error}
The pattern forces you to account for the failure case:
// Good code: won't runtime error and has no typescript errorsfunction playSong(id: string) { const song = loadSong(id) if (isErr(song)) { addElm(ErrorDiv('could not load song')) return } addElm(Player(song).play()) addElm(Metadata(song))}
Useful for preventing:
- blank screen and "button does nothing" bugs in the browser
- server timeout and bad response bugs in node
- system scripts failing in intermediate states, leaving junk behind
- unexpected errors in library code
You don't need a library
All the code for this pattern fits in a short file, and you can customize it to your needs. Here's my implementation:
// err.ts:const ERR = Symbol('ERR')type Err = { [ERR]: true error: unknown type?: ErrTypes}/** Optional addition if you want to handle errors differently based on their type */type ErrTypes = 'internet' | 'fileSystem' | 'badInput'export function isErr(x: unknown): x is Err { return typeof x === 'object' && x != null && ERR in x}export function Err(message: string, type?: string) { return { [ERR]: true, error: message, type: type }}/** Make an error-throwing function into a error-returning function */export async function tryFail<T>( f: (() => Promise<T>) | (() => T)): Promise<T | Err> { try { return await f() } catch (e) { return { [ERR]: true, error: e } }}/** If you need to convert your error values back into throw/catch land */export function assertOk<T>(x: T | Err) { if (isErr(x)) throw Error(x.error)}
I recommend putting those in the global package scope so they're always available without import.
Use the error-return pattern for external libraries & stdlib
Easiest to demonstrate with an example
/** Sometimes has error in runtime and crashes server */function getUserBad1(id: string) { const buf = readFileSync(`./users/${id}.json`) return JSON.parse(buf.toString())}/** Works but verbose: */function getUserBad2(id: string) { let buf: Buffer try { buf = readFileSync(`./users/${id}.json`) } catch (e) { console.warn('could not read file:', e) return null } let user: User try { user = JSON.parse(buf.toString()) return user } catch (e) { console.warn('could not parse user file as json') return null }}/** tryFail pattern is best of both worlds */function getUser(id: string) { const buf = tryFail(() => readFileSync(`./users/${id}.json`)) if (isErr(buf)) return buf return tryFail(() => JSON.parse(buf.toString()))}
Wrap unreliable functions to make them error-returning
If you're using some library functions all over the place and are tired of repeating the tryFail(()=>...)
everywhere (even though it beats massive try-catch chains), it can be helpful to wrap the library with error-returning logic.
We just need one more function in our error library:
// err.ts:function errReturnify<In, Out>( f: (...args: In) => Out): (...args: In) => Out | Err { return (...args: In) => { try { return f() } catch (e) { if (e instanceof Error) return Err(e.message) return Err(`unknown error in ${f.name}: ${JSON.stringify(e)}`) } }}
Then we can use it to make wrapped library:
// wrapped/fs.ts/** Wrap up error-throwing functions into error-returning ones */import { cpSync as cpSync_, mkdirSync as mkdirSync_, readFileSync as readFileSync_,} from 'fs'export const cpSync = errReturnify(cpSync_)export const mkdirSync = errReturnify(mkdirSync_)export const readFileSync = errReturnify(readFileSync_)// wrapped/JSON.tsexport default JSON = { parse: errReturnify(JSON.parse), stringify: JSON.stringify,}
Then you can use the library code with perfect elegance and reliability
// server.tsimport { readFileSync } from './wrapped/fs'import JSON from './wrapped/JSON'function getUser(id: string) { const buf = readFileSync(`./users/${id}.json`) if (isErr(buf)) return buf return JSON.parse(buf.toString())}
A little inconvenience, but it's just two lines to wrap any function f
. Worth the effort if you're using f
more than a few times.
Conclusion
In short, this simple pattern can make typescript code dramatically more reliable while avoiding the awkward empty let
s with nested try/catch that pervade typescript networking code.
Original Link: https://dev.to/qpwo/goodbye-trycatch-hello-error-return-5hcp
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To