10
Help us understand the problem. What are the problem?

posted at

updated at

await-to-js で非同期処理(Promise)の try/catch をなくそう

API 通信などのエラーハンドリングはどうしていますか?

await したものを try/catch で囲む、もしくは Promise の catch を使うなどがありますが、きちんと都度エラーハンドリングしていくとコードが複雑になりがちです。

await-to-js はそういったエラーハンドリングの辛さを解消する手段の1つとして効果的です。

それでは簡単なエラーハンドリングの例を元に説明していきます。

非同期処理のエラーハンドリング

例えば「ユーザーを取得してタスクを保存する」というものを考えてみます。

async function saveUserTask(userId) {
    // ユーザー取得
    const user = await fetchUser(userId);
    if (!user) {
        throw new Error("存在しないユーザー");
    }

    // タスク保存
    const userTask = new UserTask(userId, "タスク1");
    await userTask.save();
}

上記のコードだと、DB 接続など Promise でエラーの発生する可能性があります。
async/await 関数では、このようにエラーをハンドリングする場合 try/catch を使うことが一般的で、単純に従うと以下のようになります。

async function saveUserTask(userId) {
    // ユーザー取得
    try {
        const user = await fetchUser(userId);
        if (!user) {
            throw new Error("存在しないユーザー");
        }
    } catch (error) {
        throw new Error("ユーザー情報取得エラー");
    }

    // タスク保存
    const userTask = new UserTask(userId, "タスク1");
    try {
        await userTask.save();
    } catch (error) {
        throw new Error("タスク保存エラー");
    }
}

主要なコードとエラーハンドリングのコードが混ざり、どのような処理をする関数なのかがぱっと見でわかりづらくなりました。
もしコメントがなかったら読むのに少し抵抗のあるコードに感じるでしょう。

実際の業務だとより複雑な Promise 絡みの処理を書く可能性もあり悩ましいです。

Go のエラーハンドリング

例えば Go でファイル読み取る処理を書く場合、次のように書けます。

func main() {
    // ファイルを開く
    file, error := os.Open("tmp.txt")
    if error != nil {
        return fmt.Errorf("ファイルを開くことに失敗: %w", error)
    }

    // ファイル読み取り
    contnet, error := ioutil.ReadAll(file)
    if (error) {
        return fmt.Errorf("ファイル読み取り失敗: %w", error)
    }

    fmt.Println(string(contnet))
}

処理結果とエラーを多値で返すことで、それらを使い主要なコードとエラーハンドリングのコードを分けて書けます。
呼び出し側にエラーハンドリングの責務を負わせることもできます。

なお、Go は処理結果を返さない場合はエラーのみを返します。

Go のように書く

JavaScript でこれをやりたい場合、以下のような実装すればいけそうです。

  1. エラーが起きたら [error] を返す
  2. エラーが起きなければ [null, data] を返す

それを汎用的に使える関数にすると次のようになります。

function to(promise) {
    return promise
        .then((data) => {
            return [null, data];
        })
        .catch((error) => [error]);
}

これを使って実際に先程のコードを書き直してみましょう。
Promise の部分を to() で囲むだけです。

async function saveUserTask(userId) {
    // ユーザー取得
    const [errorFetchUser, user] = await to(fetchUser(userId));
    if (errorFetchUser) {
        throw new Error("ユーザー情報取得エラー");
    }
    if (!user) {
        throw new Error("存在しないユーザー");
    }

    // タスク保存
    const userTask = new UserTask(userId, "タスク1");
    const [errorSaveUserTask] = await to(userTask.save());
    if (errorSaveUserTask) {
        throw new Error("タスク保存エラー");
    }
}

主要なコードとエラーハンドリングの行が分かれることによって、何をしているかがわかりやすいコードになりました。

この to を提供してくれているのが await-to-js です。

TypeScript にも対応

await-to-js は TypeScript にも対応していて、to<User>(...) のようにして返り値の型を指定できます。

const [error, user] = await to<User>(fetchUser(userId));

また、 to() の返り値が Conditional Types で指定されており、エラーハンドリングをしないと処理結果が undefined のユニオン型になるため、truthy チェックが必要になります。
つまり to() で囲った場合はエラーハンドリングを強制でき、対応漏れを防ぐことができます。

const [error, user] = await to<User>(fetchUser(userId));

// if (error) でアーリーリターンをしていないため、 user が User|undefined になり型エラー
console.log(user.id);

await-to-js のコードは非常にシンプルなので軽く確認してみてください。
https://github.com/scopsy/await-to-js/blob/master/src/await-to-js.ts

余談

今回の例ではエラーを再 throw していて元のスタックトレースなどが失われています。
最近は Error に cause オプションが追加 され前回のエラー情報を渡せるようになっているので、同様にこちらも使っていくと良いです。

const [error, user] = await to(fetchUser());
if (error) {
    throw new Error("ユーザー情報取得エラー", {
        cause: error,
    });
}

さいごに

Promise のエラーハンドリング周りの設計はプロジェクトごとに様々ですが、もし都度ハンドリングしていく方針であれば await-to-js はその手助けとなるでしょう。
ぜひ採用を検討してみてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
10
Help us understand the problem. What are the problem?