# Async/await Source: https://js.maxbraglia.com/async-apis/async-await Write asynchronous code that reads like synchronous code ## The modern way to write async code `async/await` is syntactic sugar on top of Promises. It lets you write async code that reads top-to-bottom, just like synchronous code. This is what you'll use 90% of the time. ```javascript theme={null} async function getUsers() { const response = await fetch("http://localhost:8000/api/users"); const users = await response.json(); return users; } ``` Two keywords, two rules: * **`async`** goes before the function declaration — marks it as asynchronous * **`await`** goes before a Promise — pauses execution until the Promise resolves ## async functions Adding `async` to a function does one thing: it makes the function **always return a Promise**. ```javascript theme={null} async function greet() { return "Hello!"; } // Equivalent to: function greet() { return Promise.resolve("Hello!"); } // Both return a Promise greet().then(message => console.log(message)); // "Hello!" ``` This means any function that uses `await` inside it must be marked `async`. ```javascript theme={null} // ❌ Error: await is only valid in async functions function getUsers() { const response = await fetch("/api/users"); // SyntaxError! } // ✅ Correct: mark function as async async function getUsers() { const response = await fetch("/api/users"); return response.json(); } ``` ### Arrow function version ```javascript theme={null} // Regular async function async function getUsers() { const response = await fetch("/api/users"); return response.json(); } // Async arrow function const getUsers = async () => { const response = await fetch("/api/users"); return response.json(); }; ``` ## await — pausing until the Promise resolves `await` pauses the function until the Promise settles. The function doesn't block the page — it just pauses internally while other code keeps running. ```javascript theme={null} async function fetchAndLog() { console.log("1. Starting fetch..."); const response = await fetch("/api/users"); // Pauses here console.log("2. Got response"); // Runs after fetch completes const users = await response.json(); // Pauses here console.log("3. Parsed JSON"); // Runs after parsing return users; } // Meanwhile, the rest of your app stays interactive fetchAndLog(); console.log("4. This runs immediately — doesn't wait for fetchAndLog"); // Output: // 1. Starting fetch... // 4. This runs immediately — doesn't wait for fetchAndLog // 2. Got response // 3. Parsed JSON ``` `await` pauses the current `async` function, not the entire program. Other code outside the function continues to run. That's why line 4 prints before lines 2 and 3. ## Error handling with try/catch Use `try/catch` to handle errors in async functions — just like you would in Python: ```javascript theme={null} async function getUsers() { try { const response = await fetch("http://localhost:8000/api/users"); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const users = await response.json(); return users; } catch (error) { console.error("Failed to fetch users:", error.message); throw error; // Re-throw so the caller can handle it too } } ``` `try/catch` catches both: * **Network errors** — when `fetch` itself fails (no internet, DNS failure) * **Errors you throw** — like when `response.ok` is false ### try/catch/finally ```javascript theme={null} async function loadUsers() { setLoading(true); try { const response = await fetch("/api/users"); if (!response.ok) throw new Error(`HTTP ${response.status}`); const users = await response.json(); setUsers(users); } catch (error) { setError(error.message); } finally { setLoading(false); // Always runs — success or failure } } ``` `finally` is perfect for resetting loading states. It runs regardless of success or failure, so you don't need to set `loading = false` in both the `try` and `catch` blocks. ## Sequential vs parallel ### Sequential — one after another When each request depends on the previous result: ```javascript theme={null} async function getUserOrders(userId) { const userResponse = await fetch(`/api/users/${userId}`); const user = await userResponse.json(); // Need the user first to get their orders const ordersResponse = await fetch(`/api/orders?userId=${user.id}`); const orders = await ordersResponse.json(); return { user, orders }; } ``` ### Parallel — all at once When requests are independent, use `Promise.all()` with `await`: ```javascript theme={null} async function getDashboardData() { // ❌ Sequential — slow (each waits for the previous) const users = await fetch("/api/users").then(r => r.json()); const products = await fetch("/api/products").then(r => r.json()); const orders = await fetch("/api/orders").then(r => r.json()); // Total time: request1 + request2 + request3 // ✅ Parallel — fast (all run at the same time) const [users, products, orders] = await Promise.all([ fetch("/api/users").then(r => r.json()), fetch("/api/products").then(r => r.json()), fetch("/api/orders").then(r => r.json()), ]); // Total time: max(request1, request2, request3) } ``` A common mistake is using `await` for every request even when they don't depend on each other. If two requests are independent, run them in parallel with `Promise.all()`. ## Comparing to Python ```javascript theme={null} async function getUser(userId) { try { const response = await fetch(`/api/users/${userId}`); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error("Error:", error.message); throw error; } } // Call it const user = await getUser(1); ``` ```python theme={null} import httpx async def get_user(user_id: int): try: async with httpx.AsyncClient() as client: response = await client.get(f"/api/users/{user_id}") response.raise_for_status() return response.json() except httpx.HTTPError as error: print(f"Error: {error}") raise # Call it user = await get_user(1) ``` The syntax is nearly identical. The big difference: in Python, async is opt-in (most code is synchronous). In JavaScript, **any code that touches the network is async by default**. ## The pattern you'll use everywhere This is the complete async/await pattern for API calls: ```javascript theme={null} async function fetchData(endpoint) { try { const response = await fetch(`http://localhost:8000${endpoint}`); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return await response.json(); } catch (error) { console.error(`Failed to fetch ${endpoint}:`, error); throw error; } } // Usage const users = await fetchData("/api/users"); const products = await fetchData("/api/products"); ``` You'll see this exact pattern in nearly every web application. Learn it once, use it everywhere. ## What's next? You understand async/await. Now let's put it to work — making real HTTP requests to your FastAPI backend with `fetch`. Make GET, POST, PUT, and DELETE requests with fetch # Callbacks Source: https://js.maxbraglia.com/async-apis/callbacks Understand the original async pattern in JavaScript and why we moved on ## What is a callback? A callback is a function you pass to another function, to be called later when something finishes. You've already used them — `.map()`, `.filter()`, and `.forEach()` all take callbacks. ```javascript theme={null} // You already know callbacks const numbers = [1, 2, 3]; numbers.forEach(function(num) { // This function is a callback console.log(num); }); // Arrow function version (same thing) numbers.forEach(num => console.log(num)); ``` For async operations, callbacks work the same way: "When this async task finishes, call this function with the result." ```javascript theme={null} // setTimeout uses a callback console.log("Starting timer..."); setTimeout(() => { console.log("Timer finished!"); // Called after 2 seconds }, 2000); console.log("Timer is running in the background..."); // Output: // Starting timer... // Timer is running in the background... // Timer finished! ← 2 seconds later ``` ## Callbacks for async operations Before `fetch`, JavaScript used `XMLHttpRequest` with callbacks. Here's what async code looked like with callbacks: ```javascript theme={null} // Simulating async API calls with callbacks function getUser(userId, callback) { setTimeout(() => { const user = { id: userId, name: "Sarah Chen", role: "admin" }; callback(user); // Call the callback with the result }, 1000); } // Usage getUser(1, function(user) { console.log(user.name); // "Sarah Chen" — runs after 1 second }); ``` The pattern: pass a function that receives the result. The async operation calls your function when it's done. ## The error-first callback pattern Node.js standardized a pattern: the first argument to a callback is always the error (or `null` if no error). ```javascript theme={null} function getUser(userId, callback) { setTimeout(() => { if (userId <= 0) { callback(new Error("Invalid user ID"), null); return; } const user = { id: userId, name: "Sarah Chen" }; callback(null, user); // null = no error }, 1000); } // Usage getUser(1, function(error, user) { if (error) { console.error("Failed:", error.message); return; } console.log(user.name); // "Sarah Chen" }); ``` The error-first pattern (`callback(error, result)`) is a convention, not a language feature. Node.js APIs follow this pattern, but browser APIs like `fetch` use Promises instead. ## Callback hell The real problem shows up when you need multiple async operations that depend on each other — get a user, then get their orders, then get the order details: ```javascript theme={null} // ❌ Callback hell — nested callbacks getUser(1, function(error, user) { if (error) { console.error(error); return; } getOrders(user.id, function(error, orders) { if (error) { console.error(error); return; } getOrderDetails(orders[0].id, function(error, details) { if (error) { console.error(error); return; } console.log(details); // Need more? Keep nesting... }); }); }); ``` This is called **callback hell** or the "pyramid of doom." Every dependent async operation adds another level of nesting. It's: * Hard to read * Hard to debug * Hard to handle errors properly * Easy to make mistakes Compare the same logic with the modern approach you'll learn soon: ```javascript theme={null} // ✅ async/await — flat, readable try { const user = await getUser(1); const orders = await getOrders(user.id); const details = await getOrderDetails(orders[0].id); console.log(details); } catch (error) { console.error(error); } ``` Same logic, no nesting, clean error handling. This is where we're headed. ## Where you'll still see callbacks Callbacks aren't dead. You'll use them for: ```javascript theme={null} // Event listeners button.addEventListener("click", () => { console.log("Button clicked!"); }); // Array methods const names = users.map(user => user.name); // setTimeout / setInterval setTimeout(() => { console.log("Delayed action"); }, 1000); ``` These are fine because they're not being nested. The problem was using callbacks for **sequential async operations**. You don't need to master callbacks for async work. The point of this lesson is to understand *why* Promises and async/await were created. For actual async code, you'll use async/await. ## What's next? Promises were invented to solve callback hell. They give async operations a cleaner interface and let you chain operations without nesting. Handle async operations with a cleaner pattern # Error handling Source: https://js.maxbraglia.com/async-apis/error-handling Handle errors gracefully in your API calls and async code ## Why error handling matters Network requests fail. Servers go down. Users lose internet. APIs return unexpected data. If you don't handle errors, your app shows a blank screen or crashes silently. ```javascript theme={null} // ❌ No error handling — app crashes silently async function getUsers() { const response = await fetch("/api/users"); const users = await response.json(); return users; } // ✅ With error handling — app stays usable async function getUsers() { try { const response = await fetch("/api/users"); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { console.error("Failed to load users:", error); throw error; } } ``` ## try/catch/finally The core pattern for handling errors in async code: ```javascript theme={null} async function loadUserProfile(userId) { try { // Code that might fail const response = await fetch(`/api/users/${userId}`); if (!response.ok) throw new Error(`HTTP ${response.status}`); const user = await response.json(); return user; } catch (error) { // Runs if anything in try block throws console.error("Error loading profile:", error.message); return null; // Return a fallback value } finally { // Always runs — success or failure console.log("Request finished"); } } ``` ```javascript theme={null} try { const data = await riskyOperation(); } catch (error) { console.error(error.message); } finally { cleanup(); } ``` ```python theme={null} try: data = await risky_operation() except Exception as error: print(error) finally: cleanup() ``` Same structure, same behavior. Python catches specific exception types; JavaScript catches everything in one `catch` block. ## Two types of fetch errors This is where people get confused. `fetch` can fail in two different ways: ### 1. Network errors — fetch itself throws ```javascript theme={null} try { // These throw immediately — no response at all const response = await fetch("/api/users"); } catch (error) { // TypeError: Failed to fetch // Causes: no internet, DNS failure, CORS blocked, server unreachable console.error("Network error:", error.message); } ``` ### 2. HTTP errors — fetch succeeds but status is bad ```javascript theme={null} const response = await fetch("/api/users/999"); // fetch succeeded — we got a response // But the status is 404 (not found) console.log(response.ok); // false console.log(response.status); // 404 // We need to check manually and throw if (!response.ok) { throw new Error(`HTTP ${response.status}`); } ``` `fetch` only throws on **network** failures. A 404 or 500 response is not an error from `fetch`'s perspective — it successfully received a response. Always check `response.ok`. ### Handling both types ```javascript theme={null} async function getUsers() { try { const response = await fetch("/api/users"); // Type 2: HTTP error (404, 500, etc.) if (!response.ok) { const errorBody = await response.json().catch(() => ({})); const error = new Error(errorBody.detail || `HTTP ${response.status}`); error.status = response.status; // Attach status for easier handling later throw error; } return await response.json(); } catch (error) { // Type 1: Network error (no internet, CORS, etc.) // Type 2: HTTP error (thrown above) // Both are caught here console.error("Request failed:", error.message); throw error; } } ``` ## User-friendly error messages Don't show raw error messages to users. Translate them into something helpful: ```javascript theme={null} function getErrorMessage(error) { // Prefer structured data when available const status = error.status; // Network errors // Browser messages vary ("Failed to fetch", "NetworkError", etc.) if (!status && error.name === "TypeError") { return "Can't connect to the server. Check your internet connection."; } // HTTP errors if (status === 401 || error.message.includes("401")) { return "Please log in to continue."; } if (status === 403 || error.message.includes("403")) { return "You don't have permission to do that."; } if (status === 404 || error.message.includes("404")) { return "The item you're looking for doesn't exist."; } if (status === 500 || error.message.includes("500")) { return "Something went wrong on our end. Please try again."; } // Fallback return "Something went wrong. Please try again."; } // Usage try { const users = await getUsers(); } catch (error) { showNotification(getErrorMessage(error)); // User sees a friendly message } ``` ### In React ```jsx theme={null} function UserList() { const [users, setUsers] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { async function loadUsers() { try { const response = await fetch("/api/users"); if (!response.ok) { const err = new Error(`HTTP ${response.status}`); err.status = response.status; throw err; } const data = await response.json(); setUsers(data); } catch (error) { setError(getErrorMessage(error)); } finally { setLoading(false); } } loadUsers(); }, []); if (loading) return

Loading...

; if (error) return

{error}

; return ; } ``` ## Re-throwing errors Sometimes you want to handle an error *and* let the caller handle it too: ```javascript theme={null} async function getUsers() { try { const response = await fetch("/api/users"); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { console.error("API error:", error); // Log it throw error; // Re-throw so the caller can handle it too } } // Caller handles the user-facing part try { const users = await getUsers(); renderUserList(users); } catch (error) { showErrorBanner(getErrorMessage(error)); } ``` Re-throw errors when lower-level code shouldn't decide what the user sees. Log the error for debugging, then throw it up to the component that can show a user-friendly message. ## Common mistakes ```javascript theme={null} // ❌ Wrong: catching the error but doing nothing async function getUsers() { try { const response = await fetch("/api/users"); return await response.json(); } catch (error) { // Error is caught but nobody knows about it // App shows blank screen — no data, no error message } } // ✅ Correct: handle or re-throw async function getUsers() { try { const response = await fetch("/api/users"); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { console.error("Failed:", error); throw error; // Let the caller handle it } } ``` An empty `catch` block is almost always a bug. If you catch an error, either handle it (show a message, return a fallback) or re-throw it. Silent failures are the hardest bugs to find. ```javascript theme={null} // ❌ Wrong: parsing without checking status async function getUser(id) { const response = await fetch(`/api/users/${id}`); return response.json(); // Might parse a 404 error page as JSON } // ✅ Correct: check status first async function getUser(id) { const response = await fetch(`/api/users/${id}`); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `HTTP ${response.status}`); } return response.json(); } ``` ```javascript theme={null} // ❌ Confusing: mixing Promise .catch() with try/catch async function getUsers() { try { const response = await fetch("/api/users").catch(err => { throw err; // Unnecessary — try/catch already handles this }); return response.json(); } catch (error) { console.error(error); } } // ✅ Clean: just use try/catch with async/await async function getUsers() { try { const response = await fetch("/api/users"); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (error) { console.error(error); throw error; } } ``` When using async/await, stick with `try/catch`. Don't mix in `.catch()` — it adds confusion without any benefit. ## What's next? Error handling keeps your app stable. Now let's make it feel fast with loading states — showing users what's happening while requests are in flight. Show users what's happening during requests # Fetching data from APIs Source: https://js.maxbraglia.com/async-apis/fetch-basics Make HTTP requests to your FastAPI backend using fetch and async/await ## Making API calls Every web app needs to communicate with a backend. JavaScript's `fetch()` function handles HTTP requests - GET to retrieve data, POST to create data, PUT to update, DELETE to remove. You'll use this pattern in almost every React component that needs backend data. ## Basic GET request ```javascript theme={null} async function getUsers() { const response = await fetch('http://localhost:8000/api/users'); const data = await response.json(); return data; } // Usage const users = await getUsers(); console.log(users); // Array of user objects ``` `fetch()` returns a Promise, so you need `await`. The response needs to be converted to JSON with `.json()`. The `.json()` method also returns a Promise, which is why it needs `await` too. It reads the response body and parses it as JSON. ## Handling errors ```javascript theme={null} async function getUsers() { try { const response = await fetch('http://localhost:8000/api/users'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error('Failed to fetch users:', error); throw error; // Re-throw so caller can handle it } } ``` Always check `response.ok` before parsing. A 404 or 500 status won't throw an error automatically - you have to check for it. `fetch()` only throws on network errors (no internet, DNS failure, etc.), not on HTTP error status codes. A 404 or 500 response is considered a "successful" fetch. Always check `response.ok`. ## POST request with data ```javascript theme={null} async function createUser(userData) { const response = await fetch('http://localhost:8000/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(userData), }); if (!response.ok) { throw new Error('Failed to create user'); } return response.json(); } // Usage const newUser = await createUser({ name: "John Doe", email: "john@example.com" }); console.log(newUser); // { id: 1, name: "Sarah Doe", email: "sarah@example.com" } ``` POST requests need three things: 1. `method: 'POST'` - tells the server you're creating data 2. `headers` with `Content-Type: application/json` - tells the server you're sending JSON 3. `body` with `JSON.stringify()` - converts your JavaScript object to a JSON string Always use `JSON.stringify()` when sending data to an API. The body must be a string, not a JavaScript object. ## PUT and DELETE requests ```javascript theme={null} // Update a user async function updateUser(userId, updates) { const response = await fetch(`http://localhost:8000/api/users/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); if (!response.ok) throw new Error('Failed to update user'); return response.json(); } // Delete a user async function deleteUser(userId) { const response = await fetch(`http://localhost:8000/api/users/${userId}`, { method: 'DELETE', }); if (!response.ok) throw new Error('Failed to delete user'); // DELETE often returns no content, so check first if (response.status === 204) return null; return response.json(); } ``` PUT and DELETE follow the same pattern. DELETE requests usually don't have a body, and often return status 204 (No Content) instead of JSON. ## Using environment variables ```javascript theme={null} // ❌ Don't hardcode your API URL const response = await fetch('http://localhost:8000/api/users'); // ✅ Use an environment variable const API_URL = import.meta.env.VITE_API_URL; const response = await fetch(`${API_URL}/api/users`); ``` Store your API URL in a `.env` file so you can change it between development and production without modifying code. In Vite (the build tool we'll use), environment variables must start with `VITE_` to be exposed to your code. In your `.env` file: `VITE_API_URL=http://localhost:8000` ## Complete example with error handling ```javascript api/users.js theme={null} const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; export async function getUsers() { try { const response = await fetch(`${API_URL}/api/users`); if (!response.ok) { throw new Error(`Failed to fetch users: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error fetching users:', error); throw error; } } export async function createUser(userData) { try { const response = await fetch(`${API_URL}/api/users`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData), }); if (!response.ok) { throw new Error(`Failed to create user: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error creating user:', error); throw error; } } export async function updateUser(userId, updates) { try { const response = await fetch(`${API_URL}/api/users/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); if (!response.ok) { throw new Error(`Failed to update user: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error updating user:', error); throw error; } } export async function deleteUser(userId) { try { const response = await fetch(`${API_URL}/api/users/${userId}`, { method: 'DELETE', }); if (!response.ok) { throw new Error(`Failed to delete user: ${response.status}`); } if (response.status === 204) return null; return await response.json(); } catch (error) { console.error('Error deleting user:', error); throw error; } } ``` This is the pattern you'll use in every project - a separate file with all your API functions, proper error handling, and environment variables. Put all your API functions in a separate file (like `api/users.js`). This separates concerns and makes your code easier to test and maintain. ## Common mistakes ```javascript theme={null} // ❌ Wrong: Missing await async function getUsers() { const data = fetch('http://localhost:8000/api/users'); console.log(data); // Promise { } return data; } // ✅ Correct: Use await async function getUsers() { const response = await fetch('http://localhost:8000/api/users'); const data = await response.json(); return data; } ``` Without `await`, you get a Promise object, not the data. This is one of the most common async mistakes. Remember: `fetch()` returns a Promise, and so does `.json()`. ```javascript theme={null} // ❌ Wrong: Assumes request succeeded async function getUsers() { const response = await fetch('http://localhost:8000/api/users'); return response.json(); // Might fail on 404/500 } // ✅ Correct: Check for errors async function getUsers() { const response = await fetch('http://localhost:8000/api/users'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } ``` A 404 or 500 response won't throw an error automatically. Always check `response.ok` before calling `.json()`. Otherwise you might try to parse an error page as JSON and get confusing errors. ```javascript theme={null} // ❌ Wrong: Sending object directly const response = await fetch('http://localhost:8000/api/users', { method: 'POST', body: { name: "Sarah", email: "sarah@example.com" }, }); // ✅ Correct: Stringify the object const response = await fetch('http://localhost:8000/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: "Sarah", email: "sarah@example.com" }), }); ``` The `body` must be a string, not a JavaScript object. Always use `JSON.stringify()`, and don't forget the `Content-Type` header. ```javascript theme={null} // ❌ Wrong: No error handling async function getUsers() { const response = await fetch('http://localhost:8000/api/users'); return response.json(); } // ✅ Correct: Wrap in try/catch async function getUsers() { try { const response = await fetch('http://localhost:8000/api/users'); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { console.error('Failed to fetch users:', error); throw error; } } ``` Network requests can fail for many reasons - no internet, server down, CORS errors. Always wrap fetch calls in try/catch so you can handle errors gracefully. ## What's next? You can now fetch data from your FastAPI backend. Next, let's learn how to work with the responses you get back — status codes, headers, and different response formats. Parse API responses and work with different formats # Handling responses Source: https://js.maxbraglia.com/async-apis/handling-responses Parse API responses and work with different response formats ## The Response object When `fetch` completes, you get a `Response` object. It's not the data itself — it's a wrapper with metadata about the response and methods to extract the body. ```javascript theme={null} const response = await fetch("http://localhost:8000/api/users"); console.log(response.status); // 200 console.log(response.ok); // true (status 200-299) console.log(response.statusText); // "OK" console.log(response.headers); // Headers object console.log(response.url); // "http://localhost:8000/api/users" ``` ## Key response properties | Property | Type | Description | | --------------------- | ------- | -------------------------------------- | | `response.ok` | boolean | `true` if status is 200–299 | | `response.status` | number | HTTP status code (200, 404, 500, etc.) | | `response.statusText` | string | Status text ("OK", "Not Found", etc.) | | `response.headers` | Headers | Response headers | | `response.url` | string | The URL that was fetched | ### Status codes you'll see most often ```javascript theme={null} const response = await fetch("/api/users"); switch (response.status) { case 200: // OK — data returned return await response.json(); case 201: // Created — new resource created (after POST) return await response.json(); case 204: // No Content — success but no body (after DELETE) return null; case 400: // Bad Request — invalid data sent throw new Error("Invalid request data"); case 401: // Unauthorized — not logged in throw new Error("Please log in"); case 403: // Forbidden — logged in but not allowed throw new Error("Access denied"); case 404: // Not Found — resource doesn't exist throw new Error("Resource not found"); case 500: // Internal Server Error — server broke throw new Error("Server error"); } ``` You don't need to memorize all status codes. The important ones: **200** (success), **201** (created), **204** (no content), **400** (bad request), **401** (unauthorized), **404** (not found), **500** (server error). ## Checking response.ok The most important property. It's `true` for any 2xx status code: ```javascript theme={null} async function getUsers() { const response = await fetch("/api/users"); if (!response.ok) { // Status is 400, 401, 403, 404, 500, etc. throw new Error(`HTTP error: ${response.status}`); } // Status is 200, 201, etc. return response.json(); } ``` `fetch` does **not** throw an error for 404 or 500 responses. It only throws on network failures (no internet, DNS error). You must check `response.ok` yourself. This is the most common source of bugs in fetch code. ## Extracting the response body The Response object has methods to read the body in different formats: ```javascript theme={null} const response = await fetch("/api/users"); // Most common — parse as JSON const data = await response.json(); // Plain text const text = await response.text(); // Binary data (images, files) const blob = await response.blob(); // Form data const formData = await response.formData(); ``` You can only read the body **once**. After calling `.json()`, you can't call `.text()` on the same response. If you need the body in multiple formats, use `.text()` first and parse manually. ### .json() — the one you'll use 90% of the time ```javascript theme={null} async function getUsers() { const response = await fetch("http://localhost:8000/api/users"); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const users = await response.json(); // Parse JSON body into a JS object/array return users; } const users = await getUsers(); console.log(users); // [{ id: 1, name: "Sarah Chen" }, ...] ``` Remember: `.json()` returns a Promise, so you need `await`. It calls `JSON.parse()` internally. ## Reading response headers ```javascript theme={null} const response = await fetch("/api/users"); // Get a specific header const contentType = response.headers.get("Content-Type"); console.log(contentType); // "application/json" // Check total count (if your API sends it) const totalCount = response.headers.get("X-Total-Count"); console.log(totalCount); // "42" // Iterate all headers response.headers.forEach((value, name) => { console.log(`${name}: ${value}`); }); ``` FastAPI and other backends can send custom headers like `X-Total-Count` for pagination. Access them with `response.headers.get("Header-Name")`. ## Handling different response shapes Your FastAPI backend might return data in different structures. Handle them appropriately: ```javascript theme={null} // Single object const response = await fetch("/api/users/1"); const user = await response.json(); // { id: 1, name: "Sarah Chen", email: "sarah@example.com" } // Array const response = await fetch("/api/users"); const users = await response.json(); // [{ id: 1, name: "Sarah Chen" }, { id: 2, name: "John Park" }] // Paginated response const response = await fetch("/api/users?page=1&limit=10"); const result = await response.json(); // { data: [...], total: 42, page: 1, pages: 5 } const { data: users, total, pages } = result; // Empty response (204 No Content) const response = await fetch("/api/users/1", { method: "DELETE" }); if (response.status === 204) { console.log("Deleted successfully — no body to parse"); } ``` ### Handling error responses from your API FastAPI returns structured error responses. Parse them to show useful messages: ```javascript theme={null} async function createUser(userData) { const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(userData), }); if (!response.ok) { // FastAPI returns { "detail": "Error message" } const errorBody = await response.json(); throw new Error(errorBody.detail || `HTTP ${response.status}`); } return response.json(); } // Usage try { await createUser({ name: "" }); // Invalid data } catch (error) { console.log(error.message); // "Name is required" (from FastAPI) } ``` Don't just throw generic "request failed" errors. Parse the error response body — your FastAPI backend sends useful validation messages. Show those to your users. ## Complete pattern Here's how all of this comes together in a real API function: ```javascript theme={null} async function apiRequest(endpoint, options = {}) { const response = await fetch(`http://localhost:8000${endpoint}`, options); // Handle no-content responses if (response.status === 204) { return null; } // Parse the body (whether success or error) const body = await response.json(); // Throw with the server's error message if (!response.ok) { throw new Error(body.detail || `HTTP ${response.status}`); } return body; } // Usage const users = await apiRequest("/api/users"); const newUser = await apiRequest("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Sarah Chen", email: "sarah@example.com" }), }); ``` ## What's next? You know how to read responses. But what happens when things go wrong? Let's learn how to handle errors gracefully. Handle errors in your API calls and async code # Loading states Source: https://js.maxbraglia.com/async-apis/loading-states Show users what's happening while data is being fetched ## Why loading states matter When your app fetches data, there's a gap between clicking and seeing results. Without a loading state, users see either nothing (blank screen) or stale data. They don't know if the app is working. ```jsx theme={null} // ❌ No loading state — user sees blank page for 1-3 seconds function UserList() { const [users, setUsers] = useState([]); useEffect(() => { fetch("/api/users") .then(r => r.json()) .then(data => setUsers(data)); }, []); return ; // Empty list until data arrives — looks broken } ``` ## The loading state pattern Every data-fetching component needs three states: **loading**, **error**, and **data**. ```jsx theme={null} function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function loadUsers() { try { setLoading(true); const response = await fetch("/api/users"); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); setUsers(data); } catch (err) { setError(err.message); } finally { setLoading(false); } } loadUsers(); }, []); if (loading) return

Loading users...

; if (error) return

Error: {error}

; if (users.length === 0) return

No users found.

; return ( ); } ``` This is the pattern you'll use in almost every component that fetches data. Three states, three checks at the top of the return. Notice the order: check `loading` first, then `error`, then empty state, then render data. This order matters because you want to show the most relevant state. ## Loading indicators ### Simple text ```jsx theme={null} if (loading) return

Loading...

; ``` ### Spinner component ```jsx theme={null} function Spinner() { return
; } // Usage if (loading) return ; ``` ```css theme={null} .spinner { width: 24px; height: 24px; border: 3px solid #e5e7eb; border-top-color: #3b82f6; border-radius: 50%; animation: spin 0.6s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } ``` ### Inline loading (inside existing content) ```jsx theme={null} function UserList() { // ...state setup return (

Users {loading && }

{error &&

{error}

}
    {users.map(user => (
  • {user.name}
  • ))}
); } ``` Use full-page loading for initial loads and inline loading for refreshes. If the user already sees data, don't replace it with a spinner — show the spinner alongside the existing content. ## Disabling buttons during requests Prevent users from double-clicking submit buttons: ```jsx theme={null} function CreateUserForm() { const [name, setName] = useState(""); const [submitting, setSubmitting] = useState(false); async function handleSubmit(e) { e.preventDefault(); setSubmitting(true); try { const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const newUser = await response.json(); console.log("Created:", newUser); } catch (error) { console.error("Failed:", error); } finally { setSubmitting(false); } } return (
setName(e.target.value)} disabled={submitting} />
); } ``` Key details: * `disabled={submitting}` prevents the button from being clicked again * Button text changes to show progress ("Creating...") * Input is also disabled to prevent editing during submission * `finally` ensures the button re-enables even if the request fails Always disable submit buttons during requests. Without this, users can click multiple times and create duplicate entries. This is one of the most common bugs in web applications. ## The complete data fetching pattern This is the pattern you'll reuse across your entire application: ```jsx theme={null} function DataComponent() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchData() { try { setLoading(true); setError(null); const response = await fetch("/api/endpoint"); if (!response.ok) throw new Error(`HTTP ${response.status}`); const result = await response.json(); setData(result); } catch (err) { setError(err.message); } finally { setLoading(false); } } fetchData(); }, []); if (loading) return ; if (error) return ; if (!data) return ; return ; } ``` This three-state pattern (loading / error / data) shows up so often that libraries like React Query and SWR were created to automate it. For now, writing it manually teaches you what's happening under the hood. ## Refactoring into a custom hook Once you're comfortable with the pattern, extract it into a reusable hook: ```jsx theme={null} function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { async function fetchData() { try { setLoading(true); setError(null); const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); setData(await response.json()); } catch (err) { setError(err.message); } finally { setLoading(false); } } fetchData(); }, [url]); return { data, loading, error }; } // Usage — so clean! function UserList() { const { data: users, loading, error } = useFetch("/api/users"); if (loading) return

Loading...

; if (error) return

Error: {error}

; return (
    {users.map(user => (
  • {user.name}
  • ))}
); } ``` One hook, any endpoint. The same loading/error/data pattern without repeating yourself. ## What's next? You've covered the complete async & APIs section — from understanding async JavaScript to making real API calls with proper error handling and loading states. Next up: the DOM and browser APIs. You'll learn how JavaScript interacts with the page itself — selecting elements, modifying content, and responding to user actions. Understand how JavaScript sees and interacts with your page # Promises Source: https://js.maxbraglia.com/async-apis/promises Handle async operations with JavaScript Promises ## What is a Promise? A Promise is an object that represents the eventual result of an async operation. Instead of passing a callback, the async function returns a Promise — a container that will eventually hold the result (or an error). ```javascript theme={null} const promise = fetch("http://localhost:8000/api/users"); console.log(promise); // Promise { } // The data isn't here yet, but the Promise will deliver it when it arrives ``` A Promise has three states: | State | Meaning | | ------------- | --------------------------------------------------- | | **Pending** | The operation is still running | | **Fulfilled** | The operation completed successfully (has a result) | | **Rejected** | The operation failed (has an error) | Think of a Promise like a food delivery order. It starts as "pending" (being prepared). It either gets "fulfilled" (delivered successfully) or "rejected" (restaurant cancelled the order). You can't change the outcome once it's settled. ## .then() — handling the result Use `.then()` to specify what happens when the Promise fulfills: ```javascript theme={null} fetch("http://localhost:8000/api/users") .then(response => { console.log("Got response:", response.status); return response.json(); // This also returns a Promise }) .then(users => { console.log("Users:", users); }); ``` `.then()` takes a function that receives the result. It also **returns a new Promise**, which is what enables chaining. ### Chaining Promises This is the key advantage over callbacks — sequential async operations stay flat: ```javascript theme={null} // Chained .then() calls — no nesting fetch("http://localhost:8000/api/users/1") .then(response => response.json()) .then(user => fetch(`http://localhost:8000/api/orders?userId=${user.id}`)) .then(response => response.json()) .then(orders => { console.log("User orders:", orders); }); ``` Compare this to the nested callback version: ```javascript theme={null} // Callbacks — pyramid of doom getUser(1, (err, user) => { getOrders(user.id, (err, orders) => { console.log("User orders:", orders); }); }); ``` Each `.then()` returns a new Promise, so you can keep chaining. The data flows from one `.then()` to the next. ## .catch() — handling errors Use `.catch()` to handle errors anywhere in the chain: ```javascript theme={null} fetch("http://localhost:8000/api/users") .then(response => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then(users => { console.log(users); }) .catch(error => { console.error("Something failed:", error.message); }); ``` A single `.catch()` at the end handles errors from **any step** in the chain. If `fetch()` fails, or `response.ok` is false, or `.json()` fails — the error flows down to `.catch()`. Always add `.catch()` at the end of a Promise chain. Without it, errors are silently swallowed. You'll see "Uncaught (in promise)" warnings in the console, but your code won't handle them. ## .finally() — cleanup code `.finally()` runs regardless of success or failure. Use it for cleanup: ```javascript theme={null} showSpinner(); fetch("http://localhost:8000/api/users") .then(response => response.json()) .then(users => renderUserList(users)) .catch(error => showErrorMessage(error)) .finally(() => { hideSpinner(); // Runs whether request succeeded or failed }); ``` ## Promise.all() — multiple requests in parallel When you need data from multiple endpoints and they don't depend on each other, run them in parallel: ```javascript theme={null} const [users, products, orders] = await Promise.all([ fetch("http://localhost:8000/api/users").then(r => r.json()), fetch("http://localhost:8000/api/products").then(r => r.json()), fetch("http://localhost:8000/api/orders").then(r => r.json()), ]); console.log(users, products, orders); // All three results ``` `Promise.all()` takes an array of Promises and returns a single Promise that resolves when **all** of them complete. If any one fails, the whole thing rejects. ```javascript theme={null} const [users, posts] = await Promise.all([ fetch("/api/users").then(r => r.json()), fetch("/api/posts").then(r => r.json()), ]); ``` ```python theme={null} import asyncio users, posts = await asyncio.gather( fetch_users(), fetch_posts(), ) ``` JavaScript's `Promise.all()` is like Python's `asyncio.gather()`. Same concept — run multiple async operations concurrently. ## Creating your own Promises You'll rarely need to create Promises from scratch, but here's how: ```javascript theme={null} function delay(ms) { return new Promise(resolve => { setTimeout(resolve, ms); }); } // Usage await delay(2000); // Wait 2 seconds console.log("Done waiting!"); ``` The `new Promise()` constructor takes a function with two parameters: * `resolve(value)` — call when the operation succeeds * `reject(error)` — call when the operation fails ```javascript theme={null} function fetchWithTimeout(url, ms) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error("Request timed out")); }, ms); fetch(url) .then(response => { clearTimeout(timer); resolve(response); }) .catch(error => { clearTimeout(timer); reject(error); }); }); } ``` Most of the time, you'll *consume* Promises (from `fetch`, libraries, etc.), not create them. Focus on understanding `.then()`, `.catch()`, and `Promise.all()`. ## Why you'll use async/await instead Promises solved callback hell, but chaining `.then()` calls can still get verbose. The next step — async/await — lets you write the same async code in a way that reads like synchronous code: ```javascript theme={null} // Promise chain fetch("/api/users") .then(response => response.json()) .then(users => console.log(users)) .catch(error => console.error(error)); // async/await — same thing, cleaner try { const response = await fetch("/api/users"); const users = await response.json(); console.log(users); } catch (error) { console.error(error); } ``` async/await is built on top of Promises — it's syntactic sugar, not a replacement. Understanding Promises helps you understand what async/await is doing under the hood. ## What's next? Time to learn async/await — the modern syntax you'll use 90% of the time for async operations. Write async code that reads like synchronous code # Why async matters Source: https://js.maxbraglia.com/async-apis/why-async-matters Understand why JavaScript handles tasks asynchronously and what that means for your code ## The problem with waiting In Python, code runs line by line. Each line finishes before the next one starts. When you call `requests.get()`, your program stops and waits until the response comes back. ```python theme={null} # Python — synchronous (blocking) import requests print("Fetching users...") response = requests.get("http://localhost:8000/api/users") # Program stops here users = response.json() # Waits until above finishes print(f"Got {len(users)} users") # Then continues ``` This works fine for a script. But in a browser, **blocking means the entire page freezes**. No scrolling, no clicking, no animations — nothing until the request finishes. That's a terrible user experience. ## JavaScript is single-threaded JavaScript runs on a single thread — one line of code at a time. If a network request takes 3 seconds and JavaScript just waited, your entire page would be frozen for 3 seconds. ```javascript theme={null} // ❌ If JavaScript worked synchronously (it doesn't) console.log("Fetching users..."); const response = fetch("http://localhost:8000/api/users"); // Imagine this blocks for 3 seconds const users = response.json(); // Page frozen the whole time console.log(`Got ${users.length} users`); ``` JavaScript solves this with **asynchronous execution**. Instead of waiting for slow operations, JavaScript says "start this task, and I'll come back to it when it's done." Meanwhile, the page stays interactive. ## How async actually works Think of it like ordering at a restaurant: **Synchronous (Python script)**: You order food, stand at the counter staring at the kitchen, and don't do anything else until your food arrives. **Asynchronous (JavaScript)**: You order food, get a **ticket number** (a Promise), and sit down. You can check your phone, talk to friends — the restaurant will call your number when the food is ready. ```javascript theme={null} // JavaScript — asynchronous (non-blocking) console.log("1. Fetching users..."); fetch("http://localhost:8000/api/users") .then(response => response.json()) .then(users => { console.log(`3. Got ${users.length} users`); }); console.log("2. This runs while waiting!"); ``` Output: ``` 1. Fetching users... 2. This runs while waiting! 3. Got 5 users ← arrives later ``` Notice the order: line "2" runs before line "3" even though it comes after the fetch in the code. This is the fundamental concept of async JavaScript. Code doesn't necessarily run in the order it appears. ## The event loop (simplified) You don't need to understand every detail of the event loop, but here's the key idea: 1. JavaScript runs your code line by line on the **main thread** 2. When it hits an async operation (network request, timer, etc.), it hands it off to the browser 3. The browser handles the operation in the background 4. When the operation completes, the result goes into a **queue** 5. JavaScript picks up results from the queue when it's done with the current code ```javascript theme={null} console.log("Start"); setTimeout(() => { console.log("Timer done"); // Runs after current code finishes }, 0); // Even with 0ms delay! console.log("End"); // Output: // Start // End // Timer done ← even 0ms setTimeout runs after current code ``` `setTimeout` with `0` milliseconds doesn't run immediately — it runs after the current code finishes. This proves that async callbacks always wait for the main thread to be free. ## What operations are async? Not everything in JavaScript is async. Only operations that take an unpredictable amount of time: | Async (takes time) | Sync (instant) | | -------------------------------- | ------------------------------------- | | `fetch()` — network requests | Math operations | | `setTimeout()` / `setInterval()` | String manipulation | | Reading files (in Node.js) | Array methods (`.map()`, `.filter()`) | | Database queries | Object operations | | User input events | Variable assignment | In web development, the most common async operation is **fetching data from your backend**. ## Why this matters for you As a Python developer, async is the biggest mental shift. In Python, you can mostly ignore async unless you're using `asyncio`. In JavaScript, **you'll deal with async in almost every component** that fetches data. The good news: JavaScript has clean syntax for handling async code. Over the next few lessons, you'll learn the evolution: 1. **Callbacks** — the original approach (messy) 2. **Promises** — a better abstraction (cleaner) 3. **async/await** — modern syntax (cleanest, what you'll actually use) Don't try to fight async or make everything synchronous. Embrace it. Once you understand the pattern, it becomes second nature — and it's what makes web apps feel fast and responsive. ## What's next? Let's start with the original async pattern — callbacks. Understanding callbacks helps you appreciate why Promises and async/await exist. The original async pattern and why we moved on # Creating elements Source: https://js.maxbraglia.com/dom-browser/creating-elements Dynamically add new HTML elements to the page with JavaScript ## Building the page with JavaScript So far you've modified existing elements. But often you need to create new ones — render a list of users from an API, add a notification, or build a table from data. This is where `document.createElement()` comes in. ## document.createElement() Create a new element, configure it, then add it to the page: ```javascript theme={null} // 1. Create the element const card = document.createElement("div"); // 2. Configure it card.className = "user-card"; card.textContent = "Sarah Chen — Admin"; // 3. Add it to the page document.querySelector("#user-list").appendChild(card); ``` The element doesn't appear on the page until you add it to the DOM with `appendChild` or similar methods. ### A more complete example ```javascript theme={null} // Create a user card with multiple child elements const card = document.createElement("div"); card.className = "user-card"; const name = document.createElement("h3"); name.textContent = "Sarah Chen"; const email = document.createElement("p"); email.textContent = "sarah@example.com"; email.className = "email"; const deleteBtn = document.createElement("button"); deleteBtn.textContent = "Delete"; deleteBtn.className = "btn-danger"; // Build the tree card.appendChild(name); card.appendChild(email); card.appendChild(deleteBtn); // Add to the page document.querySelector("#user-list").appendChild(card); ``` This produces: ```html theme={null}

Sarah Chen

``` ## Adding elements to the page Several methods for inserting elements, each with a different position: ```javascript theme={null} const parent = document.querySelector("#container"); const newElement = document.createElement("p"); newElement.textContent = "New paragraph"; // Add to the end (most common) parent.appendChild(newElement); // Add to the beginning parent.prepend(newElement); // Add to the end (modern alternative to appendChild) parent.append(newElement); // Insert before a specific child const reference = document.querySelector("#reference-element"); parent.insertBefore(newElement, reference); // Insert relative to an element reference.before(newElement); // Before the reference reference.after(newElement); // After the reference ``` | Method | Position | Returns | | ------------------------ | --------------- | -------------------- | | `parent.appendChild(el)` | End of parent | The appended element | | `parent.append(el)` | End of parent | `undefined` | | `parent.prepend(el)` | Start of parent | `undefined` | | `el.before(newEl)` | Before `el` | `undefined` | | `el.after(newEl)` | After `el` | `undefined` | `append()` and `prepend()` are the modern alternatives. They also accept strings: `parent.append("Hello")` adds a text node. `appendChild` only accepts elements. ## Removing elements ```javascript theme={null} // Modern — call .remove() on the element const card = document.querySelector(".user-card"); card.remove(); // Older — remove through parent const parent = card.parentElement; parent.removeChild(card); // Remove all children const container = document.querySelector("#container"); container.innerHTML = ""; // Quick way to clear everything ``` ## Rendering a list from data This is the most common use case — take data (from an API, for example) and build the DOM: ```javascript theme={null} const users = [ { id: 1, name: "Sarah Chen", role: "Admin" }, { id: 2, name: "John Park", role: "Editor" }, { id: 3, name: "Alice Rivera", role: "Viewer" }, ]; const list = document.querySelector("#user-list"); users.forEach(user => { const li = document.createElement("li"); li.textContent = `${user.name} — ${user.role}`; li.dataset.userId = user.id; list.appendChild(li); }); ``` ### With innerHTML (simpler for complex HTML) ```javascript theme={null} const users = [ { id: 1, name: "Sarah Chen", role: "Admin" }, { id: 2, name: "John Park", role: "Editor" }, { id: 3, name: "Alice Rivera", role: "Viewer" }, ]; const list = document.querySelector("#user-list"); list.innerHTML = users.map(user => `
  • ${user.name} ${user.role}
  • `).join(""); ``` `innerHTML` with template literals is convenient but vulnerable to XSS if the data contains user input. For data from your own API, it's fine. For user-generated content, use `createElement` + `textContent`. ## Document fragments (batch insertions) When adding many elements, each `appendChild` triggers a page repaint. Use a `DocumentFragment` to batch insertions: ```javascript theme={null} const users = await fetch("/api/users").then(r => r.json()); const fragment = document.createDocumentFragment(); users.forEach(user => { const li = document.createElement("li"); li.textContent = user.name; fragment.appendChild(li); // Add to fragment (no repaint) }); document.querySelector("#user-list").appendChild(fragment); // Single repaint when the fragment is added to the DOM ``` For small lists (under 100 items), the performance difference is negligible. Use fragments when rendering large datasets or when you notice visible flickering during rendering. ## How React replaces this Everything in this lesson — `createElement`, `appendChild`, `innerHTML` — is what React automates. Compare: ```javascript theme={null} // Vanilla JavaScript const users = [{ name: "Sarah" }, { name: "John" }]; const list = document.querySelector("#user-list"); list.innerHTML = users.map(u => `
  • ${u.name}
  • `).join(""); ``` ```jsx theme={null} // React — same result, declarative approach function UserList({ users }) { return (
      {users.map(user => (
    • {user.name}
    • ))}
    ); } ``` React handles creating, updating, and removing DOM elements for you. You describe *what* the UI should look like; React figures out *how* to make it happen. Understanding `createElement` helps you appreciate what React does under the hood. You won't use manual DOM creation in React projects, but you'll understand error messages and debugging better. ## What's next? You can create and add elements to the page. Now let's make them interactive — responding to clicks, typing, and other user actions. Respond to user interactions like clicks and keyboard input # Browser DevTools Source: https://js.maxbraglia.com/dom-browser/dev-tools Use browser developer tools to inspect, debug, and test your JavaScript ## Your most important tool Browser DevTools is where you'll spend a significant chunk of your development time. It lets you inspect the DOM, run JavaScript, monitor network requests, and debug errors — all without leaving the browser. ### Opening DevTools | Action | Shortcut | | --------------------- | -------------------------------------------------------- | | Open DevTools | `F12` or `Ctrl+Shift+I` (Windows) / `Cmd+Option+I` (Mac) | | Open Console directly | `Ctrl+Shift+J` (Windows) / `Cmd+Option+J` (Mac) | | Inspect element | `Ctrl+Shift+C` (Windows) / `Cmd+Option+C` (Mac) | | Action | Shortcut | | --------------------- | -------------------------------------------------------- | | Open DevTools | `F12` or `Ctrl+Shift+I` (Windows) / `Cmd+Option+I` (Mac) | | Open Console directly | `Ctrl+Shift+K` (Windows) / `Cmd+Option+K` (Mac) | | Inspect element | `Ctrl+Shift+C` (Windows) / `Cmd+Option+C` (Mac) | | Action | Shortcut | | --------------------- | -------------- | | Open DevTools | `Cmd+Option+I` | | Open Console directly | `Cmd+Option+C` | Enable DevTools first: Safari → Settings → Advanced → "Show features for web developers" Right-click any element on a page and select **"Inspect"** to jump directly to that element in the Elements panel. This is the fastest way to investigate any part of a page. ## Elements panel The Elements panel shows the live DOM tree. You can inspect, edit, and experiment with any element on the page. ### What you can do ``` Elements panel lets you: ├── See the full DOM tree (expand/collapse nodes) ├── Click an element to see its styles in the Styles pane ├── Edit HTML directly (double-click any element) ├── Edit CSS in real time (change values, add properties) ├── Toggle classes on and off ├── See computed styles (what the browser actually applied) └── Check element box model (margin, padding, border) ``` ### Practical uses * **Debug layout issues**: Inspect an element, check its computed size, margin, and padding * **Test style changes**: Edit CSS values live — no need to save and reload * **Find elements**: Use `Ctrl+F` / `Cmd+F` in the Elements panel to search by text, selector, or XPath * **Check accessibility**: See element roles, ARIA attributes, and contrast ratios Changes you make in the Elements panel are temporary — they disappear when you refresh the page. This makes it safe to experiment without fear of breaking anything. ## Console panel The Console is where you'll run JavaScript interactively, see errors, and debug your code. ### Running JavaScript ```javascript theme={null} // Type these directly in the console // Access DOM elements document.querySelector("h1").textContent // "Hello, Sarah!" // Change the page document.body.style.backgroundColor = "lightblue" // Test your functions const numbers = [1, 2, 3, 4, 5]; numbers.filter(n => n > 3); // [4, 5] // Inspect objects console.log({ name: "Sarah", age: 28 }); console.table([{ id: 1, name: "Sarah" }, { id: 2, name: "John" }]); ``` ### Console methods | Method | Purpose | | ---------------------------------------- | --------------------------------- | | `console.log()` | General output | | `console.error()` | Red error message | | `console.warn()` | Yellow warning | | `console.table()` | Display arrays/objects as a table | | `console.group()` / `console.groupEnd()` | Group related logs | | `console.time()` / `console.timeEnd()` | Measure execution time | | `console.clear()` | Clear the console | ### Reading error messages When your code breaks, the console shows the error with a clickable file and line number: ``` Uncaught TypeError: Cannot read properties of null (reading 'textContent') at script.js:15:23 ``` This tells you: * **What**: Tried to read `.textContent` on something that's `null` * **Where**: `script.js`, line 15, column 23 * **Why**: Probably tried to select an element that doesn't exist (yet) If you see `null` errors when selecting elements, your JavaScript is likely running before the DOM is fully loaded. Either move your ``. ## Network panel The Network panel shows every HTTP request the page makes — HTML, CSS, JavaScript, images, and API calls. This is essential for debugging your FastAPI integration. ### What to look for * **Status codes**: 200 (success), 404 (not found), 500 (server error), CORS errors * **Request/response bodies**: Click a request to see what was sent and received * **Timing**: How long each request takes * **Headers**: Check Content-Type, Authorization, CORS headers ### Filtering API calls Click the **Fetch/XHR** filter to show only API calls (hides images, CSS, etc.). This is the view you'll use most when debugging your frontend-backend communication. ``` Network panel → Fetch/XHR filter → Click a request → Preview/Response tab ``` When debugging API issues, check the Network panel first. It shows exactly what your frontend sent and what the backend returned — no guessing required. ## Application panel The Application panel lets you inspect browser storage: * **localStorage**: Key-value pairs persisted between sessions * **sessionStorage**: Key-value pairs cleared when the tab closes * **Cookies**: Data sent with every request to the server You'll use this when working with localStorage later in this section. ## DevTools workflow for debugging When something isn't working, follow this order: Look for red error messages. They usually tell you exactly what's wrong and where. Right-click the broken element → Inspect. Check if it exists, has the right classes, and has the expected content. If data isn't showing up, check if the API request succeeded. Look at the status code and response body. When the error isn't obvious, add `console.log()` statements to trace where values go wrong. ## What's next? You know how to inspect the DOM. Now let's learn how to find specific elements in it with JavaScript. Find and target HTML elements on the page # Event listeners Source: https://js.maxbraglia.com/dom-browser/event-listeners Respond to user interactions like clicks, keyboard input, and more ## Making pages interactive An event is something that happens on the page — a click, a key press, a form submission, a mouse hover. Event listeners let you run code when these events occur. ```javascript theme={null} const button = document.querySelector("#save-btn"); button.addEventListener("click", () => { console.log("Button clicked!"); }); ``` That's the core pattern: select an element, call `addEventListener`, pass the event name and a function to run. ## addEventListener() ```javascript theme={null} element.addEventListener(eventName, handlerFunction); ``` ```javascript theme={null} const button = document.querySelector("#delete-btn"); // Inline arrow function button.addEventListener("click", () => { console.log("Deleted!"); }); // Named function (easier to remove later) function handleDelete() { console.log("Deleted!"); } button.addEventListener("click", handleDelete); ``` You can attach multiple listeners to the same element and event. They all fire in the order they were added. ## Common events | Event | Fires when | Used on | | ------------- | ------------------------------- | ----------------------------------- | | `"click"` | Element is clicked | Buttons, links, any element | | `"dblclick"` | Element is double-clicked | Any element | | `"input"` | Input value changes (real-time) | ``, `