Async and Await

JavaScript has async and await too, working just like C#'s. An async function can await a promise, such as fetch's, and read like ordinary code, with try and catch for errors. Use it to load data into the page and send a form to an API.

Journeyman · JavaScript · REST API · Updated

After this lesson you can

  • Write an async function that awaits fetch and response.json, with try and catch for errors
  • Load data from an API into the page, and send a form's data to it, without reloading the page

Key takeaways

  • An async function can await a promise, such as fetch's, and always returns a promise, as a C# one returns a Task.
  • With await, an ordinary try and catch handles both a thrown error and a fetch that gets no answer.
  • Call event.preventDefault() in a form's submit handler, then send its data with fetch, so the page never reloads.
  • Put text from a user or an API into the page with textContent, not innerHTML, so it can't run as code.

Chains of then work, but they get hard to read as the steps pile up. JavaScript has the answer you already know from C#: async and await.

The same request, awaited

async function showCategory(id) {
    const response = await fetch(`/api/categories/${id}`);
    const category = await response.json();
    console.log(`${category.categoryName}: ${category.description}`);
}

showCategory(8);
Seafood: Seaweed and fish

It works exactly like C#:

  • async before function allows await inside it. An async function always returns a promise, as a C# one returns a Task.
  • await waits for a promise and gives its result: await fetch(...) gives the response, await response.json() gives the object.
  • While it waits, the browser is free to do other things, such as respond to clicks, so the page never freezes.

Arrow functions can be async too: const showCategory = async id => { ... };.

try and catch

With await, errors are caught with an ordinary try and catch, instead of .catch(...):

async function loadCategory(id) {
    try {
        const response = await fetch(`/api/categories/${id}`);
        if (!response.ok) {
            throw new Error(`The API answered ${response.status}`);
        }
        const category = await response.json();
        console.log(category.categoryName);
    } catch (error) {
        console.log(`Couldn't load category ${id}: ${error.message}`);
    }
}

await loadCategory(7);
await loadCategory(300);
Produce
Couldn't load category 300: The API answered 404

The catch runs both for the throw (the API answered, but not with success) and for a failed fetch (no answer at all), just as with .catch.

A page that uses the API

Here's a whole page for the wwwroot folder: it lists the categories, and has a form to add one. First wwwroot/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Categories</title>
</head>
<body>
    <h1>Categories</h1>
    <ul id="categories"></ul>
    <p id="message"></p>

    <form id="add-category">
        <input id="name" placeholder="Category name" required>
        <button type="submit">Add</button>
    </form>

    <script src="app.js"></script>
</body>
</html>

Then wwwroot/app.js:

const list = document.getElementById('categories');
const message = document.getElementById('message');
const form = document.getElementById('add-category');
const nameInput = document.getElementById('name');

async function loadCategories() {
    try {
        const response = await fetch('/api/categories');
        if (!response.ok) {
            throw new Error(`The API answered ${response.status}`);
        }
        const categories = await response.json();

        list.innerHTML = '';
        for (const category of categories) {
            const li = document.createElement('li');
            li.textContent = category.categoryName;
            list.appendChild(li);
        }
    } catch (error) {
        message.textContent = `Couldn't load the categories: ${error.message}`;
    }
}

async function addCategory(event) {
    event.preventDefault();

    const response = await fetch('/api/categories', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ categoryName: nameInput.value })
    });

    if (response.ok) {
        const added = await response.json();
        message.textContent = `Added ${added.categoryName}`;
        nameInput.value = '';
        await loadCategories();
    } else {
        message.textContent = `Couldn't add it: the API answered ${response.status}`;
    }
}

form.addEventListener('submit', addCategory);
loadCategories();

What happens:

  1. The page loads, and loadCategories() runs straight away: it fetches the list, then builds an li for each category. The page appeared first, empty, and fills in a moment later.
  2. Typing Snacks and pressing Add fires the form's submit event. addCategory stops the page reloading, POSTs the new category as JSON, shows Added Snacks, and loads the list again, now with Snacks at the end.
  3. A name longer than 15 letters gets 400 from the API's validation, and the page says Couldn't add it: the API answered 400.

The page never reloads: it only ever swaps in the parts that changed. That's how modern web apps feel so quick, and it's the idea Angular builds on in the next module.

(li.textContent = ... puts the name in as plain text. With innerHTML, a category named <img src=x onerror=alert(1)> would run as code on everyone's page. When the text comes from a user or an API, always use textContent.)

Check yourself

2 questions to try on the lesson page.

So what

async and await make JavaScript's waiting look like C#'s: top to bottom, with try and catch. Fetch, check, read the JSON, change the page: that loop is how every page talks to an API.

Key terms

Cheat sheets