Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 7, 2022 03:16 am GMT

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 lets with nested try/catch that pervade typescript networking code.


Original Link: https://dev.to/qpwo/goodbye-trycatch-hello-error-return-5hcp

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To