dotry

Typed error-first try-catch convention of functions.

Usage no npm install needed!

<script type="module">
  import dotry from 'https://cdn.skypack.dev/dotry';
</script>

README

dotry

Typed error-first try-catch convention of functions.

Unlike other packages, dotry supports all JavaScript functions, which are Function, AsyncFunction, GeneratorFunction and AsyncGeneratorFunction, regardless of transpiling to es5, es2015, es2016 or other versions.

And since it's well typed in TypeScript, it is easy to just pass any function to dotry, and TypeScript will automatically infer return types of the result.

Also, dotry only packs the potential error and the return value into the form of [err, res], it DOES NOT change the outlook of the original function.

Install

npm i dotry

Example

import dotry from "dotry";

// A regular function
// `err` will be of type `Error` and `res` will be of type `string`
let [err, res] = dotry(check, "Hello, World!");
function check(str: string) {
    if (str.length === 0) {
        throw new Error("the string must not be empty");
    }
    return str;
}


// An async function
// Just for simplicity, don't forget to code in an async function on your own.
// `err` will be of type `Error` and `res` will be of type `string`
let [err, res] = await dotry(checkAsync, "Hello, World!");
async function checkAsync(str: string) {
    if (str.length === 0) {
        throw new Error("the string must not be empty");
    }
    return str;
}

// A generator function
function* iterate(data: number[]) {
    for (let num of data) {
        if (num > 9) {
            throw new RangeError(`number ${num} is out of range`);
        } else {
            yield num;
        }
    }
}

// TypeScript cannot auto infer error types, we must provided them as the first
// type argument of `dotry`, and the second argument as the return type.
// `err` will be of type `RangeError`, and `value` will be of type `number`.
for (let [err, value] of dotry<RangeError, number>(iterate, 1, 2, 3, 4)) {
    // ...
}

// A async generator function
async function* iterateAsync(data: number[]) {
    for (let num of data) {
        if (num > 9) {
            throw new RangeError(`number ${num} is out of range`);
        } else {
            yield num;
        }
    }
}

// Just for simplicity, don't forget to code in an async function on your own.
// `err` will be of type `RangeError`, and `value` will be of type `number`.
let iterator = dotry<RangeError, number>(iterate, 1, 2, 3, 4);
for await (let [err, value] of iterator) {
    // ...
}

More Well-formed

Instead of every time invoking the function by calling dotry() explicitly, it is recommended to wrap the function body instead.

function check(str: string) {
    return dotry(() => {
        if (str.length === 0) {
            throw new Error("the string must not be empty");
        }
        return str;
    });
}

function checkAsync(str: string) {
    return dotry(async () => {
        if (str.length === 0) {
            throw new Error("the string must not be empty");
        }
        return str;
    });
}

function iterate(data: number[]) {
    return dotry<RangeError, number>(function* () {
        for (let num of data) {
            if (num > 9) {
                throw new RangeError(`number ${num} is out of range`);
            } else {
                yield num;
            }
        }
    });
}

function iterateAsync(data: number[]) {
    return dotry<RangeError, number>(async function* () {
        for (let num of data) {
            if (num > 9) {
                throw new RangeError(`number ${num} is out of range`);
            } else {
                yield num;
            }
        }
    });
}

API

function dotry<E = Error, T = any, A extends any[] = any[], TReturn = any, TNext = unknown>(
    fn: (...args: A) => AsyncGenerator<T, TReturn, TNext>,
    ...args: A
): AsyncGenerator<[E, T], [E, TReturn], TNext>;

function dotry<E = Error, T = any, A extends any[] = any[], TReturn = any, TNext = unknown>(
    fn: (...args: A) => Generator<T, TReturn, TNext>,
    ...args: A
): Generator<[E, T], [E, TReturn], TNext>;

function dotry<E = Error, R = any, A extends any[] = any[]>(
    fn: (...args: A) => Promise<R>,
    ...args: A
): Promise<[E, R]>;

function dotry<E = Error, R = any, A extends any[] = any[]>(
    fn: (...args: A) => R,
    ...args: A
): [E, R];

All these signatures will pack the result of the input function (fn) to an two-element array which the first element is the potential error and the second element is the return value (or yield value).

It is worth mentioned that once you set the E argument explicitly, you should only throw error that is of the type E, and only return a value that is of type R or T. Otherwise, you should only throw type Error and the type of return value will be auto-inferred.

Dealing With Traditional Callback Style Functions

There are two ways to deal with traditional callback style functions, use util.promisify to wrap the function, or use dotry.promisify to call the function and pack the results.

import * as fs from "fs";
import * as util from "util";
import dotry from "dotry";

// These two examples are equivalent 
(async () => {
    const getStat = util.promisify(fs.stat);
    let [err, stat] = await dotry(getStat, __filename);
    // ...
})();

(async () => {
    let [
        err,
        stat
    ] = await dotry.promisify<Error, fs.Stats, [string]>(fs.stat, __filename);
    // ...
})();