Vijit Ail Software Engineer at toothsi. I work with React and NodeJS to build customer-centric products. Reach out to me on LinkedIn or Instagram.

How to write a declarative JavaScript promise wrapper

5 min read 1593

Write Declarative Promise Wrapper JavaScript

JavaScript is a single-threaded programming language, which means that it can only execute code synchronously or from top to bottom one line at a time. However, asynchronous programming was introduced to address this issue.

This core JavaScript concept enables a function to execute while waiting for other functions to finish executing. We use asynchronous functions to make API calls to the backend. We also use them to write and read to a file or database. This concept comes in handy for both server-side developers and client-side developers.

In this guide, we’ll demonstrate how to write declarative asynchronous function calls in JavaScript. We’ll also show how it helps make our code more readable and easier to maintain.

Declarative programming

Before diving into the code, let’s review the declarative programming pattern.

Declarative programming is a programming paradigm that generally shows the logic of the code but not the steps followed to get there. With this type of programming, it’s not generally obvious what’s going on behind the scenes.

Conversely, imperative programming requires writing step-by-step code, with each step explained in detail. This can provide helpful background for future developers who may need to work with the code, but it results in very long code. Imperative programming is often unnecessary; it depends on our objective.

Declarative programming can be achieved using inbuilt JavaScript methods. Declarative programming allows us to write code that is more readable and therefore, easier to understand.

For instance, with declarative programming, we do not need to use a for loop to iterate over an array. Instead, we can simply use inbuilt array methods, like map(), reduce(), and forEach().

Here’s an imperative programming example, showing a function that reverses a string using a decrementing for loop:

const reverseString = (str) => {
    let reversedString = "";

    for (var i = str.length - 1; i >= 0; i--) { 
        reversedString += str[i];
    }
    return reversedString; 
}

But, why write ten lines of code when we can achieve the same solution with just two lines of code?

We made a custom demo for .
No really. Click here to check it out.

Here’s a declarative programming version of the same code, using JavaScript inbuilt array methods:

const reverseString = (str) => {
  return str.split("").reverse().join("");  
} 

This code snippet uses two lines of code to reverse a string. It’s very short and gets straight to the point.

Promises in JavaScript

A promise is a JavaScript object that contains the results of an asynchronous function. In other words, it represents a task that has been completed or failed in an asynchronous function.

const promise = new Promise (function (resolve, reject) {
    // code to execute
})

The promise constructor takes one argument, a callback function also called the executor. The executor function takes in two callback functions: resolve and reject. If the executor function executes successfully, the resolve() method is called and the promise state changes from pending to fulfilled. If the executor function fails, then the reject() method is called, and the promise state changes from pending to failed.

To access the resolved value, use the .then () method to chain with the promise, as shown below:

promise.then(resolvedData => {
  // do something with the resolved value
})

Similarly, in the case of a rejected value, the .catch() method is used:

promise.then(resolvedData => {
  // do something with the resolved value
}).catch(err => {
  // handle the rejected value
})

Async/await

When we have several nested callbacks or .then functions, it often becomes difficult to maintain the code and its readability.

The async keyword helps us define functions that handle asynchronous operations in JavaScript. Meanwhile, the await keyword is used to instruct the JavaScript engine to wait for the function to complete before returning the results.

The async/await syntax is just syntactic sugar around promises. It helps us achieve cleaner code that’s easier to maintain.

const getUsers = async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const data = await res.json();
  return data;
}

Async/await enables promises or asynchronous functions to execute in a synchronous manner. However, it is always good practice to wrap await keyword with a try...catch block to avoid unexpected errors.

Here’s an example where we wrap the await keyword and the getUsers() function in a try...catch block, like so:

const onLoad = async () => {
  try {
    const users = await getUsers();
    // do something with the users
  } catch (err) {
    console.log(err)
    // handle the error
  }
}

Custom promise wrapper

One of the reasons that async/await is such an awesome feature in modern JavaScript is that it helps us avoid callback hell.

Still, handling errors from multiple async functions can lead to something like this:

try {
  const a = await asyncFuncOne();
} catch (errA) {
  // handle error
}

try {
  const b = await asyncFunctionTwo();
} catch (errB) {
  // handle error
}

try {
  const c = await asyncFunctionThree();
} catch (errC) {
  // handle error
}

If we add all the async functions in one try block, we’ll end up writing multiple if conditions in our catch block, since our catch block is now more generic:

try {
  const a = await asyncFuncOne();
  const b = await asyncFunctionTwo();
  const c = await asyncFunctionThree();
} catch (err) {
  if(err.message.includes('A')) {
    // handle error for asyncFuncOne
  }
  if(err.message.includes('B')) {
    // handle error for asyncFunctionTwo
  }
  if(err.message.includes('C')) {
    // handle error for asyncFunctionThree
  }
}

This makes the code less readable and difficult to maintain, even with the async/await syntax.

To solve this problem, we can write a utility function that wraps the promise and avoids repetitive try...catch blocks.

The utility function will accept a promise as the parameter, handle the error internally, and return an array with two elements: resolved value and rejected value.

The function will resolve the promise and return the data in the first element of the array. The error will be returned in the second element of the array. If the promise was resolved, the second element will be returned as null.

const promiser = async (promise) => {
  try {
    const data = await promise;
    return [data, null]
  } catch (err){
    return [null, error]
  }
}

We can further refactor the above code and remove the try...catch block by simply returning the promise using the .then() and .catch() handler methods:

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

We can see the utility usage below:

const demoPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve("Yaa!!");
    reject("Naahh!!");
  }, 5000);
});

const runApp = async () => {
  const [data, error] = await promiser(demoPromise);
  if (error) {
    console.log(error);
    return;
  }
  // do something with the data
};

runApp();

Now, let’s take a look at a real-life use case. Below, the generateShortLink function uses a URL shortener service to shorten a full-length URL.

Here, the axios.get() method is wrapped by the promiser() function to return the response from the URL shortener service.

import promiser from "./promise-wrapper";
import axios from "axios";

const generateShortLink = async (longUrl) => {
  const [response, error] = await promiser(
    axios.get(`https://api.1pt.co/addURL?long=${longUrl}`)
  );

  if (error) return null;

  return `https://1pt.co/${response.data.short}`;
};

For comparison, here’s how the function would look without the promiser() wrapper function:

const generateShortLink = async (longUrl) => {
  try {
    const response = await axios.get(
      `https://api.1pt.co/addURL?long=${longUrl}`
    );
    return `https://1pt.co/${response.data.short}`;
  } catch (err) {
    return null;
  }
};

Now, let’s complete the example by creating a form that uses the generateShortLink() method:

const form = document.getElementById("shortLinkGenerator");

const longUrlField = document.getElementById("longUrl");

const result = document.getElementById("result");

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const longUrl = longUrlField.value;
  const shortLink = await generateShortLink(longUrl);
  if (!shortLink) result.innerText = "Could not generate short link";
  else result.innerHTML = `<a href="${shortLink}">${shortLink}</a>`;
});


<!-- HTML -->
<!DOCTYPE html>
<html>
  <head>
    <title>Demo</title>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div id="app">
      <form id="shortLinkGenerator">
        <input type="url" id="longUrl" />
        <button>Generate Short Link</button>
      </form>
      <div id="result"></div>
    </div>
    <script src="src/index.js"></script>
  </body>
</html>

Here’s the complete code and demo for your reference.

So far, the promiser() function can wrap only a single async function. However, most use cases would require it to handle multiple, independent async functions.

To handle many promises we can use the Promise.all() method and pass an array of async functions to the promiser function:

const promiser = (promise) => {
  if (Array.isArray(promise)) promise = Promise.all(promise);
  return promise.then((data) => [data, null]).catch((error) => [null, error]);
};

Here’s an example of the promiser() function used with multiple async functions:

import axios from "axios";
import promiser from "./promiser";

const categories = ["science", "sports", "entertainment"];

const requests = categories.map((category) =>
  axios.get(`https://inshortsapi.vercel.app/news?category=${category}`)
);

const runApp = async () => {
  const [data, error] = await promiser(requests);
  if (error) {
    console.error(error?.response?.data);
    return;
  }
  console.log(data);
};

runApp();

Conclusion

The solutions shared in this guide for writing declarative asynchronous function calls in JavaScript are ideal for most scenarios. However, there are additional use cases that you may need to consider. For example, you might want to only handle the expected errors and throw any exceptional error that occurs during the promise execution.

There are tradeoffs to any approach. It’s important to understand and take them into consideration for your particular use case.

The knowledge shared in this article is a good entry point for creating more complex APIs and utility functions as you continue with your coding journey. Good luck and happy coding!

: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
Vijit Ail Software Engineer at toothsi. I work with React and NodeJS to build customer-centric products. Reach out to me on LinkedIn or Instagram.

2 Replies to “How to write a declarative JavaScript promise wrapper”

  1. Another way to handle the try catch if/else is to use the await keyword but use .catch on that promise if you don’t want the outer try catch involved. You can also handle it in the .catch and also re throw it if you would want to halt the execution flow.

    try {
    // business logic includes exception so nds particular handling
    const data = await something()
    .catch(th => {
    // process exception
    // rethrow if some condition
    });
    // only caught by outer bc it’s a zero sum expectation for example
    const more = await someone();
    } catch (th) {
    // handle th
    }

  2. The approach is similar to monad-transformer TaskEither

    https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html

    Actually there is a significant difference between Either.Left and Exception.

    The first one should be used for “recovable” errors, the second one — for unrecoverable.

    So it means we don’t need to avoid throwing an exception in all cases, replacing them with error-result tuple. And the promiser can help with that.

    Nevertheless, the movement to functional programming is great.

Leave a Reply