DEV Community

Cover image for Mastering Asynchronous JavaScript: Simplified Promises with Handy Utility Functions
Srijan Karki
Srijan Karki

Posted on

Mastering Asynchronous JavaScript: Simplified Promises with Handy Utility Functions

It’s been nine years since the groundbreaking ES6 standard was introduced, bringing a plethora of useful features to JavaScript. Among these, the new way of handling asynchronous operations through Promises stands out.

Initially, developers struggled with "callback hell," which was somewhat alleviated by the .then/.catch API. This evolved into the async/await syntax, making JavaScript more readable. However, even with async/await, the code could still be cumbersome:

let result;
try {
  result = await someWork();
} catch (err) {
  // handle the error
}
// do something with the result
Enter fullscreen mode Exit fullscreen mode

Two main issues persist:

  1. The need to define a variable outside of the try/catch block.
  2. Unnecessary indentation from the try/catch block.

To streamline this, inspired by Golang's error-handling, I created a simple function for JavaScript. This evolved into a set of utilities that I’ve now compiled into a package.

One-Line Promise Handling with to

Instead of handling errors within try/catch blocks, you can use the to function from the @mrspartak/promises package:

import { to } from '@mrspartak/promises'

const [error, result] = await to(somePromise)
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Infers proper return types.
  • Does not throw errors.
  • Allows flexible variable naming via array destructuring.
  • Works on both back-end and front-end, supporting the finally API.
let isLoading = true;
const [error, result] = await to(fetchData(), () => {
  isLoading = false;
});
Enter fullscreen mode Exit fullscreen mode

Timeout Promise Execution

Sometimes, you need to ensure that a promise completes within a certain timeframe, such as network requests. The timeout function in our utility library ensures a promise resolves within a specified time:

import { timeout } from "@mrspartak/promises"
import { api } from "./api"

const [error, user] = await timeout(api.getUser(), 1000, 'Timedout');
if (error) {
  // handle the error
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Prevents hanging operations.
  • Improves user experience by providing timely feedback.

Delay the Execution

Introducing the delay function, a simple utility to pause code execution for a specified time. This is useful for rate limiting, retry mechanisms, or creating artificial delays for testing.

import { delay, sleep } from "@mrspartak/promises"
import { parsePage } from "./parser"

for (let i = 0; i < 10; i++) {
  const pageData = await parsePage(i);
  await delay(1000);
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Enhances code reusability.
  • Simplifies the introduction of delays.
  • Adds flexibility for various scenarios like retry mechanisms and rate limiting.

Defer the Execution

The deferred function provides a way to manually control the resolution or rejection of a promise, giving you full control over its lifecycle.

import { deferred } from "@mrspartak/promises"
import { readStream } from "./stream"

const { promise, resolve, reject } = deferred<void>();

const stream = await readStream();
let data = '';
stream.on('data', (chunk) => {
  data += chunk;
});
stream.on('end', () => {
  resolve();
});

await promise;
console.log(data); // Data is ready
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Allows manual control over promise resolution/rejection.
  • Provides flexibility for operations where results aren't immediately available.

Retry the Execution

The retry function retries a promise-returning function a specified number of times, optionally with a delay between attempts.

import { retry } from "@mrspartak/promises"
import { apiCall } from "./api"

const [error, result] = await retry(() => apiCall(), 3, { delay: 1000 });
if (error) {
  // handle the error
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Retries failed operations, improving resilience.
  • Can be customized with additional options like backoff algorithms and hooks.

Measure the Duration of Execution

The duration function measures how long a promise takes to resolve or reject, providing insights into performance.

import { duration } from "@mrspartak/promises"
import { migrate } from "./migrate"

const migratePromise = migrate();

const [error, result, duration] = await duration(migratePromise);

console.log(`Migration took ${duration}ms`);
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Helps identify performance bottlenecks.
  • Facilitates optimization of asynchronous operations.

Conclusion

By using these utility functions from the @mrspartak/promises package, you can handle asynchronous operations in JavaScript more efficiently and elegantly. These tools not only simplify your code but also improve its readability and maintainability, making it easier to manage complex asynchronous workflows.

Top comments (0)