Understanding Async Await In JavaScript With Examples
Modern websites often need to wait for information before they can finish a task. A page may request data from a server, load an image, validate a form, or save a preference. JavaScript handles these operations asynchronously, allowing the browser to remain responsive instead of freezing while it waits.
The async and await keywords provide a readable way to work with these delayed operations. They are built on promises, but they make asynchronous code look closer to ordinary step-by-step JavaScript. This is useful for both small scripts and larger applications, including interfaces designed with calm visual hierarchy, generous whitespace, clear typography, and image-led presentation.
How Asynchronous JavaScript Works
Synchronous code runs in order, with each statement generally waiting for the previous one to finish. An asynchronous operation follows a different pattern. JavaScript starts a task, such as a network request, and continues handling other work until the result becomes available.
A promise represents the eventual outcome of that task. It can be pending, fulfilled with a value, or rejected with an error. Instead of repeatedly checking whether a request has completed, a program can attach actions that run when the promise settles.
const result = fetch("/api/articles");
result.then(response => {
console.log(response.status);
});
The fetch() function returns a promise immediately. The browser can continue processing clicks, animations, and other events while the request travels across the network. This matters for users in cities such as Sydney, Melbourne, or Perth, where connection quality can vary between home NBN services, mobile networks, and crowded public Wi-Fi.
Promises Behind Async Await
An async function always returns a promise, even when its code appears to return an ordinary value. The await keyword pauses that function until a promise settles, without blocking the entire browser thread.
async function loadArticle() {
const response = await fetch("/api/articles/42");
const article = await response.json();
return article;
}
loadArticle().then(article => {
console.log(article.title);
});
The first await waits for the HTTP response. The second waits for the response body to be converted into JavaScript data. This sequential style is easier to follow than a long chain of .then() calls, particularly when a task includes authentication, validation, transformation, and display steps.
A practical deployment workflow may also involve asynchronous operations. For example, a developer preparing a small React project can follow a React deployment walkthrough and then use async functions to load content after the application starts. Deployment itself is separate from asynchronous programming, but both depend on understanding what happens in sequence and what can happen concurrently.
Writing A Useful Async Function
A reliable asynchronous function usually has three clear stages: obtain data, check that the result is usable, and return a meaningful value. HTTP requests do not automatically reject when a server responds with a status such as 404 or 500, so the response should be checked explicitly.
async function getWeather(city) {
const response = await fetch(
`https://api.example.com/weather?city=${encodeURIComponent(city)}`
);
if (!response.ok) {
throw new Error(`Weather request failed: ${response.status}`);
}
return response.json();
}
async function showWeather() {
const weather = await getWeather("Brisbane");
document.querySelector("#weather").textContent =
`${weather.temperature}°C, ${weather.summary}`;
}
Here, getWeather() throws an error when the server response is unsuccessful. The calling function can decide whether to display a message, record the problem, or use fallback content. A page aimed at Australian visitors might request weather for Brisbane, Adelaide, or Hobart, while remembering that local conditions and time zones can affect the information shown.
Keep asynchronous functions focused. A function that fetches data, edits many unrelated parts of the document, and manages authentication becomes difficult to test. Separating data access from presentation also supports a clean visual system, where content, photography, and whitespace can change without rewriting the network logic.
Handling Errors And User Privacy
The most common error-handling pattern is try...catch. Code inside the try block may reject, throw an error, or fail while parsing data. The catch block provides a controlled response instead of leaving the user with an unhandled promise rejection.
async function loadProfile() {
try {
const response = await fetch("/api/profile");
if (!response.ok) {
throw new Error("Unable to load profile");
}
const profile = await response.json();
renderProfile(profile);
} catch (error) {
console.error(error);
showMessage("The profile could not be loaded. Please try again.");
}
}
The message shown to visitors should be clear without exposing technical details. Developers can send fuller diagnostic information to a secure logging system, while the interface uses concise language. This is especially important for Australian audiences because personal information handled by an organisation may be subject to the Privacy Act 1988 and the Australian Privacy Principles. The implementation should match the organisation’s obligations and privacy policy.
Before sending or storing personal data, check what information is collected, why it is needed, and how long it is retained. A plain-language privacy policy guide can help readers recognise details about cookies, sharing, retention, and user rights. Async code does not remove those responsibilities; it simply controls when data operations happen.
Sequential And Concurrent Requests
Multiple await statements run sequentially when placed one after another. That is appropriate when the second request needs the first result, such as loading a user record before requesting that user’s saved items.
const user = await getUser();
const orders = await getOrders(user.id);
If requests are independent, waiting for each one separately adds unnecessary delay. Promise.all() starts them together and waits until every promise fulfils. If one rejects, the combined promise rejects, so use it when all results are required.
const [news, events, profile] = await Promise.all([
fetch("/api/news").then(response => response.json()),
fetch("/api/events").then(response => response.json()),
fetch("/api/profile").then(response => response.json())
]);
A content page could use this pattern to load several independent panels at once. For example, a page explaining a harness racing guide might request an article, glossary, and responsible-play information separately. Those requests can run concurrently, while the interface displays loading states until each section is ready.
When partial results are acceptable, Promise.allSettled() is often safer. It reports whether each operation fulfilled or rejected, allowing a page to show available content while marking one unavailable panel. This can be useful for visitors on slower regional connections or during temporary service interruptions.
Practical Patterns For Everyday Projects
Async code becomes easier to maintain when loading, success, and failure states are visible in the interface. A button can be disabled while a request is active, a placeholder can reserve space for an image, and an error message can explain what happened. These details prevent sudden layout shifts and make a calm, spacious design feel deliberate rather than unfinished.
Cancellation is another useful pattern. An AbortController can stop a request when a visitor changes search terms, leaves a page, or starts a newer request. Without cancellation, an older response might arrive after a newer one and overwrite the correct result.
const controller = new AbortController();
async function search(term) {
const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`, {
signal: controller.signal
});
return response.json();
}
// Call this when the current search is no longer needed:
// controller.abort();
For demonstrations involving changing content, a page about a slots tournament guide could load schedules, rules, and explanatory sections independently. The same JavaScript pattern applies to travel updates, product catalogues, council information, or news feeds; the subject matter changes, but the promise workflow remains the same.
Recommendations For Reliable Async Code
- Check
response.okafter everyfetch()call instead of assuming a completed request was successful. - Use
try...catcharound awaited operations and provide a useful fallback message. - Run independent requests with
Promise.all()when every result is required. - Use
Promise.allSettled()when individual failures should not hide successful results. - Keep network functions separate from DOM rendering and visual presentation logic.
- Cancel obsolete requests with
AbortControllerwhen searches or filters change. - Avoid placing private keys, passwords, or sensitive personal data in browser-side JavaScript.
Testing should include slow connections, empty responses, expired sessions, malformed JSON, and server failures. Australian users may browse during a commute, rely on mobile data, or access a site across different time zones and public holidays, so a fast success path is only part of the experience.
Used thoughtfully, async and await make JavaScript easier to read while preserving the browser’s responsive event-driven model. The result is code that can load content, recover from faults, protect user information, and support a polished presentation without hiding the underlying complexity.