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.
Jump ahead:
async/await
syntaxpromise
wrapperPromise.all()
vs. Promise.allStettled()
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?
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.
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
syntaxWhen 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 } }
promise
wrapperOne 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();
Promise.all()
vs. Promise.allSettled()
This is probably a good time to clear up any confusion about two methods: Promise.all()
and Promise.allSettled()
.
Promise.all()
will only resolve when all given promises are fulfilled. If any of the promises are rejected, the Promise.all()
will reject as a whole, which is very useful in cases where the tasks depend on each other and you want to stop the sequence of functions if one fails.
To handle multiple promises, you can also use the Promise.allSettled()
method.
Promise.allSettled()
is used for multiple asynchronous tasks that are not dependent on one another. Promise.allSettled()
does not reject; instead, it waits for all promises to be complete and return, regardless of whether they were successful. Promise.allSettled()
marks itself as fulfilled as soon as all of the given promises are complete, including both fulfilled and rejected instances.
Promise.allSettled([ Promise.resolve(39), Promise.reject("An error occurred"), new Promise(resolve => setTimeout(() => resolve(100), 2000)), 70 ]).then(v => console.log(v))
As shown below, the Promise.allSettled()
method returns an array of objects that specifies the status of each promise along with its resolved value or the rejected error message.
[{ status: "fulfilled", value: 39 }, { reason: "An error occurred", status: "rejected" }, { status: "fulfilled", value: 100 }, { status: "fulfilled", value: 70 }]
Below is a wrapper function for Promise.allSettled()
that will only return the resolved values and will return null in the case of a rejected promise.
const settler = (promise) => { if (Array.isArray(promise)) { return Promise.allSettled(promise).then(settledPromises => settledPromises.map((settledPromise) => (settledPromise.status === 'fulfilled') ? settledPromise.value : null )) } return promise.then((data) => data).catch((error) => null); }; settler([ Promise.resolve(39), Promise.reject("An error occurred"), new Promise(resolve => setTimeout(() => resolve(100), 3000)), 70 ]).then(v => console.log(v)) // [39, null, 100, 70]
So which method should you use? It depends on your needs. If you want your function to continue even if some of the promises are rejected, then Promise.allSettled()
is the way to go. But if you need all of the promises to be fulfilled for your function to work as expected, you should opt for Promise.all()
.
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!
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 see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
2 Replies to "How to write a declarative JavaScript promise wrapper"
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
}
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.