Say goodbye to callback hell
Tags: webdev, javascript, tutorial, learning
Published: 2024-02-20 01:46:42

JavaScript Proxy can help solve the issue of **callback hell**. Let me show you what I mean with an example. Imagine a user trying to log in to a web application. The back-end has to verify the user's username and password, but that's just the beginning. Depending on the application's requirements, there are many other actions the back-end could take:

-   Grant access to certain features or functionalities based on the user's role or permissions.
-   Log the user's access for auditing purposes or to track usage statistics.
-   Store a session token for the user so they don't have to keep logging in repeatedly.
-   Check whether this is their first login attempt, and if so, show them an introduction tour of your app's features.

But that's not all! You could also offer them a discount or promotion if they're a new user or if they've completed a certain action within your app. Plus, you could track their behavior and use that data to improve your app's functionality or suggest personalized content in the future. The possibilities are endless!

## What is callback hell?

Let's simplify the issue by focusing on the first two actions we introduced earlier. To implement these actions, we might introduce a service called `UserService` that supports various functions for performing different actions. For example, to verify a user's username and password, we could use the following function:

```js
verifyUser(username, password, (error, userInfo) => {
    ...
});
```

The function I'm about to explain has three parameters. The first two are the username and password. The third parameter is a callback that has two parameters of its own.

The first parameter of the callback, `error`, tells us if there was an issue verifying the user. For example, if the username doesn't exist or if the password is invalid. The second parameter of the callback, `userInfo`, contains the user's information if we find it in our database.

We can use this same approach to create other similar functions for our service.

```js
getUserRoles(username, (error, roles) => {
    ...
});

logAccess(username, (error) => {
    ...
});
```

To verify the user, we can use the following sample code:

```js
const verifyUser = (username, password, callback) => {
    userService.verifyUser(username, password, (error, userInfo) => {
        if (error) {
            callback(error);
        } else {
            userService.getUserRoles(username, (error, roles) => {
                if (error) {
                    callback(error);
                }else {
                    userService.logAccess(username, (error) => {
                        if (error){
                            callback(error);
                        } else {
                            callback(null, userInfo, roles);
                        }
                    });
                }
            });
        }
    });
};
```

When we call the `verifyUser` function, it goes through several steps. First, it checks if the username and password are valid by calling the `userService.verifyUser` function. If there's an error, it immediately calls the callback with the error message.

Assuming the username and password are valid, it moves on to retrieve all roles associated with the user by calling `userService.getUserRoles`. If there's an error retrieving the roles, it immediately calls the callback with that error message.

Once all roles have been successfully retrieved, it logs the login attempt by calling `userService.logAccess`. This step also has the potential to throw errors, which will be caught and passed back through the callback.

Finally, if everything goes smoothly up until this point, it returns the user's information along with their roles by calling back with null as the first parameter and `userInfo` and `roles` as subsequent parameters.

However, this implementation of `verifyUser` suffers from callback hell. It's hard to read and follow, and it's difficult to maintain and debug. Nested callbacks make it especially tricky to handle errors, and it's not very modular. If we need to add more functionality later on, we risk breaking other parts of our codebase.

To address these issues, we can use JavaScript proxy. Proxies allow us to intercept calls to objects and add custom behavior before forwarding them on to the original object. This lets us keep our code modular while still being able to add new functionality as needed.

In the next section, we'll explore how to use proxies to improve the `verifyUser` function.

## Simplifying callbacks with JavaScript Proxy

Callback functions can make our code difficult to read and maintain. Luckily, there's a solution: JavaScript Proxy. By using a Proxy object, we can intercept and customize operations on objects. This allows us to simplify our code and avoid "callback hell".

To illustrate, let's rewrite the `verifyUser` function using Proxy. We'll wrap our `userService` object and add a layer of abstraction that handles the callbacks for us. This will make our code cleaner and easier to read.

Here's an example implementation using a Proxy:

```js
const userServiceProxy = new Proxy(userService, {
    get: (target, propKey) => {
        const originalMethod = target[propKey];
        return (...args) => {
            return new Promise((resolve, reject) => {
                originalMethod(...args, (error, result) => {
                    if (error) {
                        reject(error);
                    } else {
                        resolve(result);
                    }
                });
            });
        };
    },
});
```

In this implementation, we use a proxy object to intercept method calls on the `userService` object. This adds an extra layer of abstraction that handles callbacks.

When you call the `get` method on the `userServiceProxy` object, it returns a new `Promise` that wraps around the original method call on the `userService` object. This `Promise` resolves with the result of the method call if there are no errors, and rejects with an error message if there are.

So, when you call a method on the proxy object, it returns a `Promise` that either resolves or rejects based on the original callback from the `userService` method.

To put it simply, we're rewriting the user verification process as follows:

```js
const verifyUser = async function(username, password) {
    try {
        const userInfo = await userServiceProxy.verifyUser(username, password);
        const roles = await userServiceProxy.getUserRoles(username);
        await userServiceProxy.logAccess(username);
        return [userInfo, roles];
    } catch (error) {
        throw error;
    }
};
```

The updated version of the `verifyUser` function is a game-changer! We've simplified the code and eliminated those frustrating nested callbacks by using `async/await`. Now, when we call `verifyUser`, it returns a `Promise` that either resolves with an array containing the user information and roles (if everything goes well), or throws an exception that we can catch using a `try/catch` block if there's an error.

Here's how it works: first, we call `userServiceProxy.verifyUser` with the provided username and password as arguments. If successful, this returns a Promise that resolves with the user information. Next, we call `userServiceProxy.getUserRoles` with the same username argument, which returns a Promise that resolves with an array of roles for that user. Finally, we call `userServiceProxy.logAccess`, which logs access for auditing purposes.

If anything goes wrong during any of these operations, an error will be thrown and caught by the catch block at the end of the function. This means we can handle errors gracefully without crashing our application.

With this new implementation of `verifyUser`, we can finally say goodbye to callback hell and write cleaner, more efficient code with better error handling.

## Conclusion

To sum up, callback hell is a common issue in JavaScript that can make code hard to read and maintain. Fortunately, we can use JavaScript Proxy to simplify our code and get rid of nested callbacks. By wrapping our service methods with a Proxy object, we can handle callbacks using Promises instead. This lets us write cleaner, more readable code that's easier to maintain and troubleshoot. With this approach, we can improve our applications' quality and reduce the time spent debugging issues caused by callback hell.

---

If you found this series helpful, please consider giving the [repository](https://github.com/phuocng/javascript-proxy) a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

If you want more helpful content like this, feel free to follow me:

-   [DEV](https://dev.to/phuocng)
-   [GitHub](https://github.com/phuocng)
-   [Website](https://phuoc.ng)