Day 20: async/await & fetch: Working with APIs
✅ Today you will learn
- async functions and await
- Error handling with try/catch/finally
- fetch: GET, response.ok, status codes, JSON
- Loading, empty and error states in the UI
- POST JSON, handle validation errors, timeouts and parallel requests
🏢 In the real world
Until now the products were typed into our JavaScript. Real shops load them from a server so the owner can change prices without a developer. We built a free practice API for this course at codingeasily.in/api/sweets/ – it returns real JSON, supports search, can be made slow (?delay=2000) or broken (?fail=1), and validates orders. Today you connect to it like a professional front-end developer.
async and await
const wait = (ms, value) => new Promise(r => setTimeout(() => r(value), ms));
const getUser = id => wait(50, { id, name: 'Aarti' });
const getOrders = user => wait(50, [{ id: 1042, total: 1800 }]);
// Same flow as yesterday’s promise chain – but reads top to bottom
async function showLastOrder(userId) {
const user = await getUser(userId); // pause here until the promise settles
const orders = await getOrders(user);
return `${user.name}: last order #${orders[0].id} (₹${orders[0].total})`;
}
showLastOrder(7).then(console.log); // an async function ALWAYS returns a promise
console.log('This line runs first – await only pauses inside the async function');Errors with try/catch/finally
const wait = (ms) => new Promise(r => setTimeout(r, ms));
async function payWithUpi(amount) {
await wait(50);
if (amount > 10000) throw new Error('UPI limit exceeded for this demo');
return `₹${amount} paid via UPI ✅`;
}
async function checkout(amount) {
console.log('⏳ Processing…');
try {
const receipt = await payWithUpi(amount);
console.log(receipt);
} catch (err) {
console.log('❌ Payment failed:', err.message);
} finally {
console.log('Spinner hidden');
}
}
(async () => { await checkout(1800); await checkout(25000); })();Two awaits in a row run one after another. If the requests do not depend on each other, start them together: const [menu, offers] = await Promise.all([getMenu(), getOffers()]); – often twice as fast.
fetch: your first API call
<button id="load">Load menu from the API</button>
<pre id="out" style="max-height:220px;overflow:auto;background:#111827;color:#a7f3d0;padding:10px;border-radius:8px">Click the button…</pre>
<script>
const API = 'https://codingeasily.in/api/sweets/';
document.querySelector('#load').addEventListener('click', async () => {
const out = document.querySelector('#out');
out.textContent = 'Loading…';
try {
const response = await fetch(API); // 1. send the request
if (!response.ok) throw new Error('HTTP ' + response.status); // 2. check the status!
const products = await response.json(); // 3. read the JSON body
out.textContent = `Status ${response.status} · ${products.length} products\n\n` +
products.map(p => `${p.id}. ${p.name.padEnd(18)} ₹${p.price}/${p.unit} stock ${p.stock}`).join('\n');
} catch (err) {
out.textContent = 'Could not load the menu: ' + err.message;
}
});
</script>| Step | Code | Why |
|---|---|---|
| Send | const res = await fetch(url) | Resolves when headers arrive |
| Check | if (!res.ok) throw … | fetch does NOT reject on 404/500 – only on network failure |
| Read | await res.json() / res.text() | The body is read separately, also async |
| Handle | try/catch around everything | Offline, timeouts, bad JSON, server errors |
| Status | Meaning | Show the user |
|---|---|---|
| 200 OK / 201 Created | Success | The data / “Order placed” |
| 400 Bad Request / 422 | Invalid input | Field-level errors |
| 401 / 403 | Not logged in / not allowed | Login prompt |
| 404 Not Found | Wrong URL or missing item | “Product not found” |
| 429 Too Many Requests | Rate limited | “Please wait and retry” |
| 500 / 502 / 503 | Server problem | “Something went wrong, try again” + retry button |
Loading, empty, error and success states
<style>
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; margin-top: 10px; }
.card { border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; background: #fff; }
.card img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; background: #f3f4f6; }
.card div { padding: 8px 10px; font-size: .9rem; }
.muted { color: #6b7280; }
.error { background: #fee2e2; color: #7f1d1d; padding: 10px; border-radius: 8px; }
.skeleton { height: 170px; border-radius: 12px; background: linear-gradient(90deg, #eee 25%, #f6f6f6 50%, #eee 75%); background-size: 200% 100%; animation: sh 1.2s linear infinite; }
@keyframes sh { to { background-position: -200% 0; } }
</style>
<input id="q" placeholder="Search (try kaju, box, xyz)" aria-label="Search">
<select id="cat" aria-label="Category"><option value="">All</option><option value="milk">Milk</option><option value="dry">Dry</option><option value="box">Gift boxes</option></select>
<label><input type="checkbox" id="slow"> Slow network</label>
<label><input type="checkbox" id="fail"> Server error</label>
<div id="view" aria-live="polite"></div>
<script>
const API = 'https://codingeasily.in/api/sweets/';
const view = document.querySelector('#view');
function showLoading() {
view.innerHTML = '<div class="grid">' + '<div class="skeleton"></div>'.repeat(4) + '</div>';
}
function showError(message) {
view.innerHTML = '';
const box = document.createElement('div');
box.className = 'error';
box.textContent = '⚠️ ' + message + ' ';
const retry = document.createElement('button');
retry.textContent = 'Try again';
retry.addEventListener('click', load);
box.append(retry);
view.append(box);
}
function showProducts(products) {
if (products.length === 0) { view.innerHTML = '<p class="muted">No sweets match your search 🙁</p>'; return; }
const grid = document.createElement('div');
grid.className = 'grid';
for (const p of products) {
const card = document.createElement('article');
card.className = 'card';
card.innerHTML = '<img alt=""><div><b></b><br><span class="muted"></span></div>';
card.querySelector('img').src = p.image;
card.querySelector('b').textContent = p.name;
card.querySelector('.muted').textContent = p.stock > 0 ? `₹${p.price}/${p.unit}` : 'Sold out';
grid.append(card);
}
view.replaceChildren(grid);
}
async function load() {
const params = new URLSearchParams(); // builds ?q=kaju&category=dry safely
const q = document.querySelector('#q').value.trim();
const cat = document.querySelector('#cat').value;
if (q) params.set('q', q);
if (cat) params.set('category', cat);
if (document.querySelector('#slow').checked) params.set('delay', 1500);
if (document.querySelector('#fail').checked) params.set('fail', 1);
showLoading();
try {
const res = await fetch(`${API}?${params}`);
if (!res.ok) throw new Error(`The server had a problem (HTTP ${res.status}).`);
showProducts(await res.json());
} catch (err) {
showError(err.message.startsWith('The server') ? err.message : 'You seem to be offline.');
}
}
document.querySelectorAll('#q, #cat, #slow, #fail').forEach(el => el.addEventListener('change', load));
document.querySelector('#q').addEventListener('keyup', e => { if (e.key === 'Enter') load(); });
load();
</script>Loading (skeletons), empty (“no results”), error (with a retry button) and success. Most beginner apps only build success – then show a blank screen on slow 4G. Interviewers and users notice the difference.
Sending data: POST an order
<form id="order">
<input name="name" placeholder="Name" value="Aarti">
<input name="phone" placeholder="Mobile" value="98290">
<button>Place order (2 kg Kaju Katli)</button>
</form>
<pre id="res" style="background:#f8fafc;padding:10px;border-radius:8px"></pre>
<script>
const API = 'https://codingeasily.in/api/sweets/';
document.querySelector('#order').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = e.currentTarget.querySelector('button');
const out = document.querySelector('#res');
const form = Object.fromEntries(new FormData(e.currentTarget));
btn.disabled = true; // prevent double orders
out.textContent = 'Sending…';
try {
const res = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...form, items: [{ id: 1, qty: 2 }] }),
});
const data = await res.json();
if (res.status === 422) { // server-side validation errors
out.textContent = '⚠️ ' + data.error + '\n' + Object.entries(data.fields).map(([f, m]) => `• ${f}: ${m}`).join('\n');
} else if (!res.ok) {
throw new Error('HTTP ' + res.status);
} else {
out.textContent = `✅ ${data.message}\nOrder ${data.orderId} · total ₹${data.total} (delivery ₹${data.delivery})`;
}
} catch (err) {
out.textContent = '❌ Could not send the order: ' + err.message;
} finally {
btn.disabled = false;
}
});
</script>Timeouts and cancelling with AbortController
async function fetchJSON(url, { timeout = 8000, ...options } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, { ...options, signal: controller.signal });
const data = await res.json().catch(() => null); // body may be empty
if (!res.ok) {
const error = new Error(data?.error || `HTTP ${res.status}`);
error.status = res.status;
error.data = data;
throw error;
}
return data;
} catch (err) {
if (err.name === 'AbortError') throw new Error('The server took too long. Please try again.');
throw err;
} finally {
clearTimeout(timer);
}
}
// usage
const products = await fetchJSON('https://codingeasily.in/api/sweets/?category=dry', { timeout: 5000 });Browsers only let a page read responses from another domain if that server allows it with an Access-Control-Allow-Origin header. Our practice API sends it, so it works from any page. If you see a CORS error with another API, it must be fixed on the server (or call it through your own backend) – not in front-end code.
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
How do you handle errors with fetch?
Wrap in try/catch; network errors and aborts reject, but HTTP errors do not, so check response.ok/status and throw; parse error bodies for messages; show user-friendly states with retry; add timeouts via AbortController; log details for developers.
What is CORS?
Cross-Origin Resource Sharing: a browser security rule that blocks a page from reading responses from another origin unless the server allows it with Access-Control-Allow-* headers. Preflight OPTIONS requests are sent for non-simple requests such as JSON POSTs.
Summary
- async/await = readable promises; wrap in try/catch/finally.
- fetch → check res.ok → await res.json().
- Always build loading, empty, error (+ retry) and success states.
- POST JSON with headers; handle 422 field errors; use AbortController for timeouts.
Day 21: modules, npm and modern tooling – organise code into files and use Vite like real teams.
