CSCI 4513
Week 5, Lecture 10
Simple tasks connected to vast hidden complexity, like async operations that seem simple but touch complex systems.
The Tale:
At Utgard-Loki's fortress, Thor faced seemingly simple challenges. He tried to drain a drinking horn but barely lowered it. He tried to lift a cat but could barely raise one paw. He wrestled an old woman who brought him to his knee.
Utgard-Loki then revealed the truth: The horn was connected to the ocean—Thor had lowered sea levels worldwide. The cat was Jormungandr, the World Serpent—Thor had lifted it off the ocean floor. The woman was Old Age itself—no one defeats time. Thor's "failures" were extraordinary achievements; he just couldn't see what he was really facing.
// Seems simple - just fetch data
async function getUserData() {
const response = await fetch('https://api.example.com/user');
return await response.json();
}
// But you're really dealing with:
// - Network latency (horn to ocean)
// - Server processing time
// - Potential failures
// - Rate limits
// - Authentication
// Promises - tasks that take time
function drinkFromHorn() {
return new Promise((resolve) => {
setTimeout(() => resolve("Lowered sea level"), 3000);
});
}
async/await - Makes async code look simple, but hides complex network operations underneath
new Promise - Creates a task that will complete later. resolve signals it's done.
setTimeout - Simulates a delay (like network latency). Runs the callback after 3000ms.
Like Thor's horn — a simple sip connected to the entire ocean!
Click each card to reveal thoughts!
Thor's horn looked simple but connected to the ocean. What makes API calls more complex than they appear?
Consider: Network latency, server processing, multiple layers of infrastructure, authentication systems, rate limiting, potential failures at any point. A simple fetch() involves DNS lookup, TCP connection, HTTP request/response, and data parsing.
Thor didn't know what he was really facing. How does async/await help reveal what's happening?
Consider: async/await makes asynchronous code read like synchronous code, making the flow clearer. You can see exactly where your code waits for responses. try/catch blocks make error handling explicit. It reveals the sequential dependencies in your async operations.
Utgard-Loki's challenges all took time. Why can't we just wait synchronously for network requests?
Consider: Synchronous waiting would freeze the entire browser—no scrolling, clicking, or animation. Network requests can take seconds. JavaScript is single-threaded, so blocking code stops everything. Async lets the browser stay responsive while waiting for external resources.
Some operations take time: fetching data from servers, reading files, waiting for user input.
// Everything waits
getData(); // Takes 3 seconds
updateUI(); // Can't run yet!
// Browser is frozen!
// Things happen in background
getData(); // Starts, keeps going
updateUI(); // Runs immediately!
// Browser stays responsive!
Sync - Each line waits for the previous one. If getData() takes 3 seconds, the entire browser freezes.
Async - getData() starts in the background. The rest of your code keeps running while it completes.
JS is single-threaded — blocking code means no scrolling, clicking, or animations!
A callback is a function passed into another function as an argument.
// Callback example you already know:
myDiv.addEventListener("click", function() {
console.log("clicked!");
});
// The function is the callback
// It runs LATER when the click happens
Callback - A function passed as an argument. It doesn't run now — it runs later when the event happens.
You've used these! - Event listeners are callbacks. The function runs when the user clicks, not when the code is read.
⚠️ Problem: Chaining multiple callbacks creates "callback hell" — deeply nested, hard to read.
getData(function(data) {
processData(data, function(processed) {
saveData(processed, function(result) {
sendEmail(result, function(sent) {
logActivity(sent, function(logged) {
// We're 5 levels deep!
});
});
});
});
});
The "pyramid of doom" - Each async step nests another callback inside the previous one. 5 steps = 5 levels deep.
Problems:
- Hard to read and follow the logic
- Difficult to handle errors at each level
- Painful to debug and maintain
This is why we need a better way: Promises!
A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation.
// Without promises - callback hell
getData(function(data) {
processData(data, function(result) {
console.log(result);
});
});
// With promises - cleaner!
getData()
.then(data => processData(data))
.then(result => console.log(result))
.catch(error => console.error(error));
.then() - Runs when the previous step succeeds. Chains are flat, not nested!
.catch() - Handles errors from any step in the chain. One handler for everything.
Key difference - Promises chain vertically (flat) instead of nesting horizontally (deep). Same logic, much easier to read.
Watch this visualization to see how promises work:
API = Application Programming Interface
Servers that serve data for external use in websites or apps.
Most APIs require an API key for access.
// Without key - Error!
https://api.example.com/weather/london
// With key - Success!
https://api.example.com/weather/london?key=abc123xyz
?key= - Query parameter appended to the URL. The API checks this before returning data.
How to get one: Sign up on the API provider's website. They'll give you a unique key string.
⚠️ Keep keys secret! In production, API keys go on the server, not in frontend code.
Modern way to make HTTP requests in JavaScript:
// fetch returns a Promise!
fetch('https://api.example.com/data')
.then(response => response.json()) // Parse JSON
.then(data => console.log(data)) // Use data
.catch(error => console.error(error));
fetch(url) - Sends an HTTP request and returns a Promise that resolves to a Response object
response.json() - Also returns a Promise! Parses the response body as JSON.
Two .then() calls - First resolves the network request, second resolves the JSON parsing. Both are async!
const img = document.querySelector('img');
fetch('https://api.giphy.com/v1/gifs/translate?api_key=YOUR_KEY&s=cats')
.then(function(response) {
return response.json();
})
.then(function(data) {
// Drill down to the URL we need
img.src = data.data.images.original.url;
})
.catch(function(error) {
console.error('Oops!', error);
});
?api_key=...&s=cats - Query params: API key for auth, s is the search term
data.data.images.original.url - APIs return nested JSON. You must drill into the structure to find the value you need.
Tip: console.log(data) first to explore the response structure before trying to access nested properties.
async/await makes asynchronous code read like synchronous code.
getData()
.then(data => {
return processData(data);
})
.then(result => {
console.log(result);
});
async function run() {
const data = await getData();
const result = await processData(data);
console.log(result);
}
Same behavior, different syntax - Both versions do exactly the same thing. async/await is just "syntactic sugar" over Promises.
async - Marks a function as asynchronous. Required to use await inside it.
await - Pauses until the Promise resolves, then returns its value. Reads like synchronous code!
// async makes a function return a Promise
async function getData() {
return "data"; // Wrapped in Promise.resolve()
}
// These are equivalent:
async function getData() {
return "data";
}
function getData() {
return Promise.resolve("data");
}
async - Keyword before function. The function now always returns a Promise.
Promise.resolve() - Wraps a value in a resolved Promise. That's what async does automatically.
Always a Promise - Even return "data" becomes a Promise. Callers must use .then() or await to get the value.
async function getCats() {
// await pauses until Promise resolves
const response = await fetch('https://api.giphy.com/...');
// This waits for the line above
const data = await response.json();
// Now we can use the data
return data.data.images.original.url;
}
// Call it
getCats().then(url => img.src = url);
await fetch(...) - Pauses here until the network request completes. Returns the Response.
await response.json() - Pauses again to parse the JSON body. Returns a plain JS object.
.then() on call - Since getCats() is async, it returns a Promise. Use .then() or await to get its result.
⚠️ Rule: await only works inside async functions!
async function getCats() {
try {
const response = await fetch('https://api.giphy.com/...');
const data = await response.json();
img.src = data.data.images.original.url;
} catch (error) {
// Handle any errors that occurred
console.error('Failed to get cats!', error);
}
}
// OR use .catch() on the function call
getCats().catch(error => console.error(error));
try { } - Wrap risky code here. If any await rejects, execution jumps to catch.
catch (error) - Receives the error object. Show a user-friendly message, log it, or retry.
Two options: try/catch inside the function, or .catch() on the call. Both work — use whichever fits your code better.
Always handle errors! Network requests can fail for many reasons.
Create a weather forecast site using the Visual Crossing API.
📅 Next Class: Coding Competition Day
📝 Homework: Weather App using Visual Crossing API