DEV Community

Cover image for How to write a declarative JavaScript promise wrapper
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

How to write a declarative JavaScript promise wrapper

Written by Vijit Ail✏️

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; 
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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]
  }
}
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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}`;
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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]);
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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!


LogRocket: 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 Dashboard Free Trial Banner

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!

Try it for free.

Top comments (0)