The Await-to-Js

    Feb 14, 2024

    Handling asynchronous function errors and result more elegantly.

    The ES6 standard of JavaScript introduced async-await to avoid callback hell. Since then, we've been able to write sequential calls to a series of asynchronous functions in a chain and handle errors uniformly at the end of the chain.

    asyncFun1()
      .then((res1) => {
        //scope 1
        return asyncFunc2(res1)
      })
      .then((res2) => {
        //scope 2
        return asyncFunc3(res2)
      })
      .then((res3) => {
        //scope 3
        return asyncFunc4(res3)
      })
      .....
      .catch((err)=>{console.log(err)})
    

    However, there's an issue with this approach: we can't access the result of the previous asynchronous function in the next scope of the chain. For instance, in the example above, we can't access res1 in scope 2 or res2 in scope 3.

    So, what if we want to pass res1 to asyncFunc3 instead of res2, or if we want to pass res2 to asyncFunc4 instead of res3?

    To address this, we introduce the await mechanism to retrieve results from higher-level asynchronous function calls in the subsequent lower-level ones.

    So, our code can be modified as follows:

    const res1 = await asyncFun1();
    const res2 = await asyncFun2(res1);
    const res3 = await asyncFun3(res1);
    const res4 = await asyncFun4(res2);
    

    But this introduces another issue: how do we handle errors when awaiting a Promise?

    Consider this function:

    // The function to calculate a / b.
    async function divide(a, b) {
      return new Promise((resolve, reject) => {
        if (b !== 0) resolve(a / b);
        else reject(new Error("The divisor cannot be 0."));
      });
    }
    

    The standard approach is:

    try {
      const res = await divide(5, 4);
    } catch (err) {
      console.log(err);
    }
    

    Typically, in JavaScript, we use a try-catch block around the await statement to catch errors.

    However, there's another problem: TypeScript doesn't support indicating the type of the parameter for the reject function. In other words, TypeScript cannot recognize the type of err in the above code and treats it as any. Even if you pass a specific Error class to reject, TypeScript still recognizes err as any.

    If reject doesn't pass an Error but instead a string, like this:

    async function divide(a, b) {
      return new Promise((resolve, reject) => {
        if (b !== 0) resolve(a / b);
        // Reject with string rather than Error here.
        else reject("The divisor cannot be 0.");
      });
    }
    

    Then, using try-catch to catch errors wouldn't be appropriate.

    Of course, you can enforce programming standards and conventions in your own projects or libraries to ensure that the parameter passed to reject is always a "Catchable Error." But what if you're using a third-party library?

    There's another scenario: you have to nest each await statement within a try-catch block every time, which leads to poor code flexibility and readability, making it bulky.

    try {
      const res1 = await asyncFun1();
    } catch (err) {
      console.log(err);
    }
    
    try {
      const res2 = await asyncFun2();
    } catch (err) {
      console.log(err);
    }
    
    try {
      const res3 = await asyncFun3();
    } catch (err) {
      console.log(err);
    }
    
    // ......
    

    Let's think of an alternative. Is it possible to create a function that awaits an asynchronous function and returns a tuple, providing the result if resolved and the error if rejected?

    That brings us to today's topic: implementing an await-to function to uniformly obtain both resolved and rejected results without having to use try-catch every time.

    function to(promise) {
      return new Promise((resolve) => {
        promise.then((res) => resolve([null, res])).catch((err) => resolve([err, null]));
      });
    }
    
    // Example Usage
    const examplePromise = new Promise((resolve, reject) => {
      // Simulate the Async operation
      setTimeout(() => {
        reject(new Error("Something went wrong"));
      }, 1000);
    });
    
    const [err, res] = await to(examplePromise);
    console.log([err, res]); // Now it will return the [Error,null]
    
    // Or if the promise is resolved.
    
    // Example Usage
    const examplePromise = new Promise((resolve, reject) => {
      // Simulate the Async operation
      setTimeout(() => {
        resolve("Hello");
      }, 1000);
    });
    
    const [err, res] = await to(examplePromise);
    console.log([err, res]); // Now it will return the [null,"Hello"]
    

    This way, we've implemented await-to. It's very friendly to annotated error types and the implementation is quite elegant. Of course, this method of handling exceptions resembles GoLang!

    There are already packages related to await-to-js on npm that you can use out of the box.

    npm i await-to-js
    

    Its source code and examples are open-source on GitHub.