Skip to main content

Command Palette

Search for a command to run...

Async/Await in JavaScript: Structural Clarity over Callback Chaos

Updated
3 min readView as Markdown

Why async/await Was Introduced

Problem Before

JavaScript handled asynchronous operations using:

  • Callbacks → led to callback hell (nested, hard-to-read code)

  • Promises → improved structure but still chained (.then().catch())

Example (Promise chaining):

fetchData()
  .then(data => processData(data))
  .then(result => saveData(result))
  .catch(err => handleError(err));

Core Issue

  • Control flow is fragmented

  • Error handling is non-linear

  • Cognitive overhead increases with complexity

Solution

async/await was introduced (ES2017) to:

  • Flatten asynchronous code

  • Make it resemble synchronous execution

  • Improve readability and maintainability

How Async Functions Work

Definition

An async function:

  • Always returns a Promise

  • Wraps return values into Promise.resolve()

  • Converts thrown errors into Promise.reject()

async function example() {
  return 42;
}

Equivalent:

function example() {
  return Promise.resolve(42);
}

Key Property

Inside an async function:

  • You can use await

  • Execution pauses until a Promise settles

Await Keyword Concept

What await Does

  • Pauses execution of the async function

  • Waits for a Promise to resolve/reject

  • Returns the resolved value

async function getData() {
  const data = await fetchData();
  console.log(data);
}

Under the Hood

  • await does not block the thread

  • It yields control back to the event loop

  • Execution resumes when Promise resolves

Important Constraint

  • await works only inside async functions

Error Handling with Async Code

With Promises

fetchData()
  .then(data => processData(data))
  .catch(err => console.log(err));

With Async/Await

async function run() {
  try {
    const data = await fetchData();
    const result = await processData(data);
  } catch (err) {
    console.log(err);
  }
}

Comparison: Promises vs Async/Await

Aspect Promises Async/Await
Syntax .then().catch() await + try/catch
Readability Moderate High
Error Handling Distributed Centralized
Debugging Harder Easier
Control Flow Chained Sequential-like

Async/Await as Syntactic Sugar

Concept

async/await is not a new async model
It is built on top of Promises.

This:

async function run() {
  const data = await fetchData();
  return data;
}

Is internally similar to:

function run() {
  return fetchData().then(data => data);
}

Key Insight

  • Same engine behavior

  • Different developer experience

Readability Improvement Example

Without Async/Await

getUser()
  .then(user => getOrders(user.id))
  .then(orders => getTotal(orders))
  .then(total => console.log(total))
  .catch(err => console.log(err));

With Async/Await

async function run() {
  try {
    const user = await getUser();
    const orders = await getOrders(user.id);
    const total = await getTotal(orders);
    console.log(total);
  } catch (err) {
    console.log(err);
  }
}

Observation

  • Reads top-to-bottom

  • Easier to reason about execution

Simple Async Example

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function run() {
  console.log("Start");
  await delay(2000);
  console.log("End after 2 seconds");
}

run();

Execution Flow Diagrams

Promise Flow

Start
  ↓
fetchData()
  ↓
.then()
  ↓
.then()
  ↓
.catch()
  ↓
End

Async/Await Flow

Start
  ↓
await fetchData()
  ↓
await processData()
  ↓
try/catch handles errors
  ↓
End

Async Function Execution Model

Call async function
  ↓
Returns Promise immediately
  ↓
Execution runs until first await
  ↓
Pause (non-blocking)
  ↓
Resume after Promise resolves
  ↓
Continue execution
  ↓
Resolve final Promise

Conclusion

async/await addresses structural issues in asynchronous JavaScript by:

  • Reducing nesting

  • Simplifying control flow

  • Centralizing error handling

More from this blog