top of page

From Callbacks to Clean Code

ree

Asynchronous programming is essential in modern application development – whether you're building real-time dashboards, handling API requests, or integrating with third-party services. It enables you to manage user interactions, data fetching, and other non-blocking operations without freezing the applications. 


Although JavaScript is single-threaded, it achieves concurrency using asynchronous APIs. Developers rely on Promises and async/await to write non-blocking code while background tasks run independently, keeping applications efficient and responsive. 


Understanding Asynchronous Techniques 

What is a Promise? 

A Promise represents a value that may be available now, in the future, or never. It provides a cleaner, more structured way to handle asynchronous operations compared to traditional callbacks, helping avoid issues like callback nesting and improving code readability. 

A Promise goes through three states

  • pending – the initial state, where the operation is still in progress 

  • fulfilled – the operation completed successfully and returned a value 

  • rejected – the operation failed and returned an error 


Here’s a simple example of a Promise that resolves after one second: 

const fetchData = () => { 
  return new Promise((resolve,reject) => { 
    setTimeout(() => { 
      resolve("Data loaded"); 
    },1000); 
  }); 
}; 

fetchData().then(console.log); 
// → "Data loaded" (after 1 second) 

What is async? 

Adding the async keyword before a function automatically wraps its return value in a Promise.  


Here’s the same example rewritten using async instead of manually creating a Promise: 

async function fetchData() { 
  return "Data loaded"; 
} 
fetchData().then(console.log); 
// → "Data loaded" 

Even though fetchData() returns a plain string, marking the function as async makes it return a Promise that resolves with that string. 


What is await? 

The await keyword pauses execution inside an async function until a Promise resolves, while letting the rest of the program continue running normally. 


Here’s the same fetchData example written using await: 

function fetchData() { 
  return new Promise((resolve) => { 
    setTimeout(() => { 
      resolve("Data loaded"); 
    }, 1000); 
  }); 
} 
async function run() { 
  console.log("Waiting..."); 
  const result = await fetchData(); 
  console.log(result); 
} 
run(); 
// → "Waiting..." immediately 
// → "Data loaded" after 1 second 

Challenges in Asynchronous E-Commerce Workflows 

E-commerce dashboards often require multiple dependent asynchronous tasks as each step relies on the data from the previous one. For example, authenticating users before accessing the profile, completing user profile before placing an order, etc. 


Using traditional callback-based patterns, the code might look like this where each step nests inside the previous one creating callback hell


function authenticateUser(userId, callback) { 
  setTimeout(() => { console.log("User authenticated"); callback(userId); }, 1000);
}

function getUserProfile(userId, callback) { 
  setTimeout(() => { console.log("Fetched profile for user:", userId); callback({ id: userId, name: "Alice" }); }, 1000); 
}

function getOrderHistory(user, callback) { 
  setTimeout(() => { console.log("Fetched order history for:", user.name); callback(["Order1", "Order2"]); }, 1000); 
} 

// Callback hell 
authenticateUser(1, (userId) => { 
  getUserProfile(userId, (user) => { 
    getOrderHistory(user, (orders) => { 
      console.log("Orders:", orders); 
    }); 
  }); 
}); 

Even for a few steps, the code quickly becomes nested and hard to maintain


Promises: A Step Forward 

Promises help solve callback hell by letting you chain asynchronous operations in a clear, readable way. For example: 

authenticateUser(1) 
  .then(getUserProfile) 
  .then(getOrderHistory) 
  .then((orders) => console.log("Orders:", orders)) 
  .catch((error) => console.error("Something went wrong:", error)); 

Using async/await for Cleaner Asynchronous Code 

Promises handle asynchronous tasks, but multiple .then() calls can become hard to read, whereas async/await lets you write them in a clear, top-down style. 

async function loadDashboard() { 
  try { 
    const userId = await authenticateUser(1); 
    const user = await getUserProfile(userId); 
    const orders = await getOrderHistory(user); 
    console.log("Orders:", orders); 
  } catch (error) { 
    console.error("Failed to load dashboard:", error); 
  } 
} 
loadDashboard(); 

Advanced Patterns 

Below are some practical guidelines to handle asynchronous tasks efficiently and avoid common pitfalls: 


1. Sequential vs Parallel Execution 

Run sequentially only if tasks depend on previous results; otherwise, parallel execution is faster. Use Promise.allSettled() to handle partial failures. 

Sequential (slower, dependent tasks): 

const user = await getUser(); 
const userSettings = await getUserSettings(user.id); 

Parallel (faster, independent tasks): 

const [user, globalSettings] = await Promise.all([ 
  getUser(), 
  getGlobalSettings(), 
]); 

2. Error Handling 

Always handle rejections inside async functions using try...catch: 

If a Promise inside an async function rejects and you don’t handle it, it becomes an unhandled rejection, which may crash your app. 

async function fetchData() { 
  try { 
    const data = await riskyOperation(); 
  } catch (error) { 
    console.error("Operation failed:", error); 
  } 
} 

You can combine with .catch() for hybrid usage: 

fetchData().catch(console.error); 

3. Loops with Async Operations 

Avoid .forEach() with async/await for it doesn’t wait for Promises. Use for...of instead: 

for (const item of items) { 
  await processItem(item); 
} 

Or run in parallel with Promise.all(): 

await Promise.all(items.map((item) => processItem(item))); 

4. Prevent Promises from Hanging 

Ensure Promises always resolve or reject; otherwise, memory leaks can occur in long-running apps: 

const safePromise = new Promise((resolve, reject) => { 
  doSomethingAsync().then(resolve).catch(reject); 
}); 

Add a timeout if necessary: 

function withTimeout(promise, ms) { 
  return Promise.race([ 
    promise, 
    new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms)),
  ]); 
} 
await withTimeout(fetchData(), 5000); 

5. Consistent Style 

Avoid mixing .then() and async/await within the same function; pick one for clarity. 

async function main() { 
  const result = await doSomething(); 
  const data = await fetchMore(result); 
  return process(data); 
} 
main().then(console.log).catch(console.error); 

Use .then() for legacy APIs; use async/await for new code. 


6. When not to use async/await 

  • Simple one-off operations – .then() may be shorter. 

  • Complex branching logic – Promise chains may be more expressive. 

  • Performance-critical code – Promise.all() or event-driven patterns may be better. 


Real-World Async Examples 

Below are some examples showing how async/await handles real-world scenarios: 

1. Retry Logic for API Calls 

async function fetchWithRetry(url, retries = 3) { 
  for (let i = 0; i < retries; i++) { 
    try { 
      const res = await fetch(url); 
      if (!res.ok) throw new Error(res.statusText); 
      return await res.json(); 
    } catch (err) { 
      console.log(`Attempt ${i + 1} failed. Retrying...`); 
    } 
  } 
  throw new Error("All retries failed"); 
} 

2. Handling Multiple APIs with Some Failing 

const results = await Promise.allSettled([ 
  fetch("/api/user"), 
  fetch("/api/stats"), 
  fetch("/api/notifications"), 
]); 

3. Sequential Processing with Delay 

async function processOrders(orders) { 
  for (const order of orders) { 
    await handleOrder(order); 
    await new Promise((res) => setTimeout(res, 500)); // add delay 
  } 
} 

4. Background Task with Progress 

async function loadData(progressCallback) { 
  const total = 5; 
  for (let i = 1; i <= total; i++) { 
    await fetchDataPart(i); 
    progressCallback((i / total) * 100); 
  } 
} 

With the asynchronous applications getting increasingly complex, understanding and using async/await smartly will make your code clean, readable, maintainable and efficient. 


References 

Comments


bottom of page