# JavaScript async / await: Promises, Syntax, and Common Pitfalls

Source: https://devdocs10.com/articles/javascript/js-async-await  
By Murat Bo · Published 2026-07-13 · DevDocs10 (https://devdocs10.com)

JavaScript `async` / `await` is a cleaner way to write asynchronous code that still uses Promises under the hood. It helps you read network requests, timers, file-like operations in Node.js, and other delayed work in a style that looks more like normal synchronous code.

> **Quick answer:** Mark a function with `async` to make it return a Promise, then use `await` inside it to pause execution until another Promise settles. Use `try...catch` to handle failures.

> **Difficulty:** Beginner to Intermediate
>
> **You'll understand this better if you know:** basic JavaScript functions, what a Promise is, and how values are assigned to variables.

## 1. What Is async / await?

`async` / `await` is syntax for working with Promises in a more direct style. It does not replace Promises; it makes Promise-based code easier to read and write.

- `async` goes before a function declaration or function expression.
- `await` can be used only inside an `async` function in standard JavaScript code.
- `await` pauses the function until the Promise settles, then gives you the resolved value.
- If the Promise rejects, `await` throws an error that you can catch with `try...catch`.

This pattern is especially useful when one async operation must finish before the next one starts.

## 2. Why async / await Matters

Before `async` / `await`, Promise chains often became long and nested. That made code harder to follow, especially when you needed multiple steps in sequence.

`async` / `await` matters because it:

- reduces callback-style nesting in Promise-heavy code,
- makes sequential async logic easier to read,
- fits naturally with `try...catch` for error handling,
- works with almost every Promise-based API in browsers and Node.js.

It is not always the fastest way to do everything, but it is often the clearest way to express intent.

## 3. Basic Syntax

The minimal pattern is an `async` function that uses `await` on a Promise.

### Function declaration

Here is the core shape of the syntax:

```javascript
async function loadData() {
  const result = await fetchData();
  return result;
}
```

In this pattern, `async` makes the function return a Promise automatically. The `await` expression waits for `fetchData()` to resolve, then stores the resolved value in `result`.

### What each part means

- `async function` tells JavaScript that the function may pause and resume.
- `await` can be placed before any expression that returns a Promise.
- `return` inside an async function wraps the value in a resolved Promise.

## 4. Step-by-Step Examples

### Example 1: Waiting for a Promise to resolve

Start with a Promise that resolves after a delay. Then wait for it inside an async function.

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

async function run() {
  console.log("Start");
  await delay(1000);
  console.log("One second later");
}

run();
```

Output (tested with Node 24.21.0):

```text
Start
One second later
```

This example shows that `await` pauses only the `run` function, not the whole program.

### Example 2: Reading a network response with fetch

The browser `fetch()` API returns a Promise, so it is a natural fit for `await`.

```javascript
async function loadProfile() {
  const response = await fetch("/api/profile");
  const profile = await response.json();

  return profile;
}
```

Here, the first `await` waits for the HTTP response, and the second waits for the JSON body to be parsed.

### Example 3: Handling errors with try...catch

When a Promise rejects, `await` throws an error. Use `try...catch` to handle that failure.

```javascript
async function loadUser() {
  try {
    const response = await fetch("/api/user");

    if (!response.ok) {
      throw new Error("Request failed");
    }

    return await response.json();
  } catch (error) {
    console.error("Could not load user:", error);
    return null;
  }
}
```

This pattern is useful because it keeps success and failure handling in one place.

### Example 4: Sequential versus parallel awaits

Sometimes you want one operation after another. Other times, you want multiple independent Promises to run together.

```javascript
async function loadDashboard() {
  // Sequential: each step waits for the previous one.
  const user = await fetchUser();
  const settings = await fetchSettings(user.id);

  // Parallel: both promises start before waiting for either one.
  const [posts, notifications] = await Promise.all([
    fetchPosts(),
    fetchNotifications()
  ]);

  return { user, settings, posts, notifications };
}
```

This shows an important skill: `await` is great for sequencing, but independent work is often better with `Promise.all`.

## 5. Practical Use Cases

Use `async` / `await` when your code needs to wait for asynchronous work that already returns a Promise.

- Loading API data in a browser before rendering a page section.
- Submitting a form and waiting for a server response.
- Reading files in Node.js with Promise-based APIs.
- Running multi-step workflows where each step depends on the previous one.
- Combining several independent requests with `Promise.all` inside an async function.

If the code does not need to wait for anything, `async` / `await` is unnecessary overhead.

## 6. Common Mistakes

### Mistake 1: Using await outside an async function

Many beginners try to use `await` in a regular function or at the top level of a classic script.

**Problem:** In ordinary scripts and non-async functions, JavaScript reports a syntax error such as `await is only valid in async functions and the top level bodies of modules`.

```javascript
function loadData() {
  const response = await fetch("/api/data");
  return response;
}
```

**Fix:** Mark the function as `async`, or move the code into a module if you need top-level `await`.

```javascript
async function loadData() {
  const response = await fetch("/api/data");
  return response;
}
```

The corrected version works because `await` is now inside an async function.

### Mistake 2: Forgetting that async functions return Promises

An `async` function does not return a plain value immediately. It always returns a Promise.

**Problem:** Code that expects a direct value can fail or produce `undefined` behavior because the function result is still a Promise object.

```javascript
async function getName() {
  return "Ada";
}

const name = getName();
console.log(name.toUpperCase());
```

Output (tested with Node 24.21.0):

```text
Uncaught TypeError: name.toUpperCase is not a function
```

**Fix:** Wait for the Promise with `await`, or use `then()` if you are outside an async function.

```javascript
async function showName() {
  const name = await getName();
  console.log(name.toUpperCase());
}
```

The fixed version works because `name` now holds the resolved string instead of a Promise.

### Mistake 3: Not handling rejected Promises

When a Promise rejects and nobody catches it, the failure becomes an unhandled rejection.

**Problem:** This often shows up as `Uncaught (in promise)` in the console, which means the error escaped without a handler.

```javascript
async function loadData() {
  const response = await fetch("/api/missing");
  return await response.json();
}

loadData();
```

**Fix:** Catch errors inside the async function, or handle the returned Promise where you call it.

```javascript
async function loadData() {
  try {
    const response = await fetch("/api/missing");
    return await response.json();
  } catch (error) {
    console.error(error);
    return null;
  }
}

loadData().catch((error) => {
  console.error("Unexpected failure:", error);
});
```

The corrected version works because every rejection has a clear place to go.

## 7. Best Practices

### Practice 1: Use async / await for readable sequencing

When one async step depends on the previous result, `await` makes the dependency obvious.

```javascript
async function loadOrder(id) {
  const order = await fetchOrder(id);
  const shipping = await fetchShipping(order.shippingId);
  return { order, shipping };
}
```

This reads like a step-by-step recipe, which is easier to maintain than nested callbacks or deeply chained Promises.

### Practice 2: Use Promise.all for independent operations

If two requests do not depend on each other, start them together and wait once.

```javascript
async function loadPageData() {
  const [user, notifications] = await Promise.all([
    fetchUser(),
    fetchNotifications()
  ]);

  return { user, notifications };
}
```

This is usually faster than awaiting each Promise one by one, because both operations can run in parallel.

### Practice 3: Keep error handling close to the await

Handle expected failures near the code that can fail. This makes the behavior easier to reason about.

```javascript
async function saveProfile(profile) {
  try {
    const response = await fetch("/api/profile", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(profile)
    });

    if (!response.ok) {
      throw new Error("Save failed");
    }

    return true;
  } catch (error) {
    console.error("Could not save profile", error);
    return false;
  }
}
```

This pattern keeps failure handling local and prevents silent rejection bugs.

## 8. Limitations and Edge Cases

- `await` does not make code magically parallel; if you write several awaits in a row, they still run one after another.
- `await` only works with thenable values and Promises; awaiting a plain value returns it immediately.
- An `async` function always returns a Promise, even if you `return` a simple value.
- In older scripts, top-level `await` is not allowed unless the code runs as a module.
- Using `await` inside `Array.prototype.forEach` does not wait the way many beginners expect, because `forEach` does not handle Promises.

> **Note:** If you need ordered async processing over a list, use `for...of` with `await`, or collect Promises and combine them with `Promise.all`.

## 9. Practical Mini Project

Let’s build a tiny browser example that loads a message and shows success or failure in the page.

```html
<button id="loadButton">Load message</button>
<p id="status">Idle</p>

<script>
  const button = document.getElementById("loadButton");
  const status = document.getElementById("status");

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

  async function loadMessage() {
    try {
      status.textContent = "Loading...";
      await delay(1000);
      status.textContent = "Message loaded successfully.";
    } catch (error) {
      status.textContent = "Something went wrong.";
      console.error(error);
    }
  }

  button.addEventListener("click", loadMessage);
</script>
```

This mini project combines a Promise, an async function, and a user action. It shows the full pattern you will use often in real browser code.

## 10. Key Points

- `async` functions always return Promises.
- `await` pauses only the current async function, not the entire JavaScript runtime.
- Use `try...catch` to handle rejected Promises cleanly.
- Use `Promise.all` when tasks can run at the same time.
- Top-level `await` depends on module support.

## 11. Practice Exercise

Write an async function that:

- waits for two independent Promises,
- returns both results together,
- handles errors without crashing,
- prints a friendly message if either Promise fails.

**Expected output:** an object or message showing both resolved values when successful, or an error message when one request fails.

**Hint:** Use `Promise.all` for the two independent operations, then wrap the code in `try...catch`.

```javascript
function getUser() {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id: 1, name: "Ada" }), 300);
  });
}

function getSettings() {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ theme: "dark" }), 400);
  });
}

async function loadUserData() {
  try {
    const [user, settings] = await Promise.all([
      getUser(),
      getSettings()
    ]);

    return { user, settings };
  } catch (error) {
    console.error("Failed to load data:", error);
    return null;
  }
}

loadUserData().then((data) => {
  console.log(data);
});
```

Output (tested with Node 24.21.0):

```text
{ user: { id: 1, name: "Ada" }, settings: { theme: "dark" } }
```

This solution works because both Promises start together, and the `catch` path prevents an unhandled rejection.

## 12. Final Summary

`async` / `await` is a Promise-friendly way to write asynchronous JavaScript that is easier to read than many nested Promise chains. It is best for code that needs to wait for values in sequence, especially when one step depends on the result of the previous step.

Remember that `async` does not remove asynchrony, and `await` does not make multiple tasks run in parallel by itself. Use `Promise.all` for independent work, and use `try...catch` to keep errors under control.

If you want to go further, the next useful topic is Promises themselves, especially `Promise.all`, rejection handling, and how async functions behave when they return values or throw errors.

## Browser Support

| Feature | Status | Chrome | Edge | Firefox | Safari | Safari iOS |
| --- | --- | --- | --- | --- | --- | --- |
| Async functions | Widely available since Oct 2019 | 55+ | 15+ | 52+ | 10.1+ | 10.3+ |
| Top-level await | Newly available since Sep 2026 | 89+ | 89+ | 89+ | 27+ | 27+ |

Data: web-features 3.43.0 (MDN Baseline).
