# JavaScript Async/Await Explained: From Callbacks to Clean Code

### Introduction
JavaScript often has to wait — for a network request, a file, or a timer. The old way to handle waiting was nested callbacks, which quickly became hard to read. Promises improved that. Async/await makes it read almost like normal, top-to-bottom code.
If you understand promises and one keyword pair, `async` and `await`, you can write most asynchronous JavaScript you will need day to day.
### The basic pattern
A function marked `async` always returns a promise. Inside it, `await` pauses that function until a promise settles, without blocking the rest of the program.
```js
async function getUser(userId) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/users/${userId}`
  );
  const user = await response.json();
  return user;
}
getUser(1).then((user) => console.log(user.name));
```
Read it like normal code: fetch the user, wait for the JSON, return the user. The `await` lines are where the waiting happens, and they are easy to spot.
### Handling errors with try/catch
Without error handling, a failed request becomes an unhandled promise rejection. Wrap awaited calls in `try/catch`, just like synchronous code.
```js
async function getUserSafely(userId) {
  try {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/users/${userId}`
    );
    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }
    return await response.json();
  } catch (error) {
    console.error("Could not load user:", error.message);
    return null;
  }
}
```
Two habits matter here. First, check `response.ok`, because `fetch` only rejects on a network failure, not on a 404 or 500. Second, return a safe fallback, like `null`, so the caller can handle the missing data cleanly.
### Sequential vs parallel
Awaiting in a loop runs tasks one after another. That is correct when one task depends on the previous one, but slow when the tasks are independent.
```js
// Sequential: second request waits for the first
const user1 = await getUser(1);
const user2 = await getUser(2);
// Parallel: both start at once, then wait for both
const [userA, userB] = await Promise.all([getUser(1), getUser(2)]);
```
Use `Promise.all` when the tasks do not depend on each other. Note that if any promise in `Promise.all` rejects, the whole thing rejects, so handle errors for the group, not just for one item.
### Common beginner mistakes
- Forgetting `await`, then wondering why a variable holds a promise instead of data.
- Using `await` inside a normal function instead of an `async` function.
- Awaiting inside a loop when the tasks are independent — use `Promise.all` instead.
- Forgetting error handling, so one failed request crashes the flow silently.
### Takeaway
Mark the function `async`, `await` the promises you need, handle errors with `try/catch`, and use `Promise.all` for independent work. That covers most real async code, without callback nesting.
Originally published at https://practicaldevblog.blogspot.com/2026/10/javascript-async-await-explained.html
