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
asyncfunction canawaita promise, such asfetch's, and always returns a promise, as a C# one returns aTask. - With
await, an ordinarytryandcatchhandles both a thrown error and afetchthat gets no answer. - Call
event.preventDefault()in a form'ssubmithandler, then send its data withfetch, so the page never reloads. - Put text from a user or an API into the page with
textContent, notinnerHTML, 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#:
asyncbeforefunctionallowsawaitinside it. An async function always returns a promise, as a C# one returns aTask.awaitwaits 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:
- The page loads, and
loadCategories()runs straight away: it fetches the list, then builds anlifor each category. The page appeared first, empty, and fills in a moment later. - Typing Snacks and pressing Add fires the form's
submitevent.addCategorystops the page reloading,POSTs the new category as JSON, shows Added Snacks, and loads the list again, now with Snacks at the end. - A name longer than 15 letters gets
400from 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.