Mastering Asynchronous JavaScript: Simplified Promises with Handy Utility Functions
Tags: webdev, javascript, beginners, programming
Published: 2024-06-24 04:09:15



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:

```javascript
let result;
try {
  result = await someWork();
} catch (err) {
  // handle the error
}
// do something with the result
```

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:

```javascript
import { to } from '@mrspartak/promises'

const [error, result] = await to(somePromise)
```

#### 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.

```javascript
let isLoading = true;
const [error, result] = await to(fetchData(), () => {
  isLoading = false;
});
```

### 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:

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

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

#### 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.

```javascript
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);
}
```

#### 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.

```javascript
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
```

#### 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.

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

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

#### 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.

```javascript
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`);
```

#### 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.