4.7: Asynchronous JavaScript
Master form validation and user input handling using JavaScript. Learn how to validate email addresses, check required fields, provide user feedback, and create robust, user-friendly forms.
1. Synchronous vs Asynchronous
Synchronous (Blocking)
Code executes line by line, waiting for each operation to complete:
console.log("First");
console.log("Second");
console.log("Third");
// Output:
// First
// Second
// Third
Problem: Slow operations block everything:
console.log("Start");
// Simulate slow operation (blocking)
for (let i = 0; i < 1000000000; i++) {} // Takes 1-2 seconds
console.log("End");
// Page freezes during loop!
Python comparison:
# Python is also synchronous by default
print("First")
print("Second")
print("Third")
# Blocking operation
import time
time.sleep(2) # Blocks for 2 seconds
print("After sleep")
Asynchronous (Non-blocking)
Operations run in the background, allowing other code to execute:
console.log("Start");
setTimeout(() => {
console.log("Delayed message");
}, 2000); // Wait 2 seconds
console.log("End");
// Output:
// Start
// End
// Delayed message (after 2 seconds)
Common async operations:
- Network requests (fetch API)
- File reading (Node.js)
- Timers (setTimeout, setInterval)
- Database queries
- User interactions (events)
2. Callbacks
Callback = Function passed as argument to another function, executed later.
Basic Callback
function greet(name, callback) {
console.log(`Hello, ${name}`);
callback();
}
greet("Alice", () => {
console.log("Callback executed!");
});
// Output:
// Hello, Alice
// Callback executed!
Python comparison:
def greet(name, callback):
print(f"Hello, {name}")
callback()
greet("Alice", lambda: print("Callback executed!"))
Async Callbacks
console.log("Start");
setTimeout(() => {
console.log("Timeout callback");
}, 1000);
console.log("End");
// Output:
// Start
// End
// Timeout callback (after 1 second)
Callback Hell (Pyramid of Doom)
Problem: Nested callbacks become unreadable:
setTimeout(() => {
console.log("Step 1");
setTimeout(() => {
console.log("Step 2");
setTimeout(() => {
console.log("Step 3");
setTimeout(() => {
console.log("Step 4");
// Keep nesting... š±
}, 1000);
}, 1000);
}, 1000);
}, 1000);
Python asyncio comparison:
import asyncio
async def steps():
await asyncio.sleep(1)
print("Step 1")
await asyncio.sleep(1)
print("Step 2")
# Much cleaner!
Solution: Use Promises or async/await (covered next).
3. Promises
Promise = Object representing eventual completion (or failure) of an async operation.
Promise States
A promise has three states:
- Pending - Initial state, not fulfilled or rejected
- Fulfilled - Operation completed successfully
- Rejected - Operation failed
Creating a Promise
const promise = new Promise((resolve, reject) => {
// Async operation
const success = true;
if (success) {
resolve("Success!"); // Fulfill promise
} else {
reject("Error!"); // Reject promise
}
});
promise
.then(result => {
console.log(result); // "Success!"
})
.catch(error => {
console.log(error);
});
Real Example: Simulated API Call
function fetchUser(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: "Alice" });
} else {
reject("Invalid user ID");
}
}, 1000);
});
}
fetchUser(1)
.then(user => {
console.log("User:", user);
})
.catch(error => {
console.log("Error:", error);
});
Chaining Promises
fetchUser(1)
.then(user => {
console.log("User:", user.name);
return user.id; // Pass to next then()
})
.then(userId => {
console.log("User ID:", userId);
return userId * 2;
})
.then(result => {
console.log("Result:", result);
})
.catch(error => {
console.log("Error:", error);
});
// Output:
// User: Alice
// User ID: 1
// Result: 2
Key point: Each .then() returns a new promise, allowing chaining.
finally()
Runs regardless of success or failure:
fetchUser(1)
.then(user => console.log("User:", user))
.catch(error => console.log("Error:", error))
.finally(() => {
console.log("Cleanup done"); // Always runs
});
4. Async/Await (Modern Approach)
async/await = Syntactic sugar over promises, making async code look synchronous.
async Functions
async function greet() {
return "Hello";
}
// Equivalent to:
function greet() {
return Promise.resolve("Hello");
}
// Usage
greet().then(msg => console.log(msg)); // "Hello"
Key: async function always returns a promise.
await Keyword
await pauses execution until promise resolves (only works in async functions):
async function fetchData() {
console.log("Fetching...");
const user = await fetchUser(1); // Wait for promise
console.log("User:", user);
console.log("Done!");
}
fetchData();
// Output:
// Fetching...
// (wait 1 second)
// User: { id: 1, name: "Alice" }
// Done!
Without async/await (promises):
function fetchData() {
console.log("Fetching...");
fetchUser(1).then(user => {
console.log("User:", user);
console.log("Done!");
});
}
Python comparison:
import asyncio
async def fetch_user(user_id):
await asyncio.sleep(1)
return {"id": user_id, "name": "Alice"}
async def fetch_data():
print("Fetching...")
user = await fetch_user(1)
print("User:", user)
print("Done!")
asyncio.run(fetch_data())
Error Handling with try/catch
async function fetchData() {
try {
const user = await fetchUser(-1); // Will fail
console.log("User:", user);
} catch (error) {
console.log("Error:", error); // "Invalid user ID"
}
}
fetchData();
Compare to promise .catch():
fetchUser(-1)
.then(user => console.log("User:", user))
.catch(error => console.log("Error:", error));
Sequential vs Parallel Execution
Sequential (one after another):
async function sequential() {
const user1 = await fetchUser(1); // Wait 1 second
const user2 = await fetchUser(2); // Wait 1 second
console.log(user1, user2);
// Total: 2 seconds
}
Parallel (simultaneously):
async function parallel() {
const promise1 = fetchUser(1); // Start both
const promise2 = fetchUser(2);
const user1 = await promise1; // Wait for first
const user2 = await promise2; // Wait for second (already running)
console.log(user1, user2);
// Total: 1 second
}
// Or use Promise.all() (better)
async function parallelAll() {
const [user1, user2] = await Promise.all([
fetchUser(1),
fetchUser(2)
]);
console.log(user1, user2);
// Total: 1 second
}
5. Promise Methods
Promise.all()
Wait for all promises to resolve:
const promise1 = fetchUser(1);
const promise2 = fetchUser(2);
const promise3 = fetchUser(3);
Promise.all([promise1, promise2, promise3])
.then(users => {
console.log("All users:", users);
// [{ id: 1, ... }, { id: 2, ... }, { id: 3, ... }]
})
.catch(error => {
console.log("One failed:", error);
// If ANY promise fails, entire Promise.all fails
});
With async/await:
async function getAllUsers() {
try {
const users = await Promise.all([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
console.log("All users:", users);
} catch (error) {
console.log("Error:", error);
}
}
Promise.race()
Returns first promise to settle (resolve or reject):
const slow = new Promise(resolve => setTimeout(() => resolve("Slow"), 2000));
const fast = new Promise(resolve => setTimeout(() => resolve("Fast"), 500));
Promise.race([slow, fast])
.then(result => {
console.log(result); // "Fast" (wins the race)
});
Use case: Timeout for slow operations:
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject("Timeout"), ms)
);
}
Promise.race([
fetchUser(1),
timeout(2000)
])
.then(user => console.log("User:", user))
.catch(error => console.log("Error:", error));
Promise.allSettled()
Wait for all promises to settle (doesn't fail if one fails):
Promise.allSettled([
fetchUser(1), // Success
fetchUser(-1), // Failure
fetchUser(2) // Success
])
.then(results => {
console.log(results);
/*
[
{ status: "fulfilled", value: { id: 1, ... } },
{ status: "rejected", reason: "Invalid user ID" },
{ status: "fulfilled", value: { id: 2, ... } }
]
*/
});
Promise.any()
Returns first successful promise (ignores rejections):
Promise.any([
Promise.reject("Error 1"),
Promise.resolve("Success!"),
Promise.reject("Error 2")
])
.then(result => {
console.log(result); // "Success!"
})
.catch(error => {
// Only if ALL fail
console.log("All failed:", error);
});
6. The Event Loop
How JavaScript Handles Async
JavaScript is single-threaded but handles async via the event loop.
Components:
- Call Stack - Executes synchronous code
- Web APIs - Handle async operations (setTimeout, fetch, etc.)
- Callback Queue - Stores callbacks from completed async operations
- Event Loop - Moves callbacks from queue to stack when stack is empty
Example:
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");
// Output:
// 1
// 3
// 2 (even with 0ms delay!)
Why?
console.log("1")executessetTimeoutgoes to Web API (even with 0ms)console.log("3")executes- Call stack is empty
- Event loop moves setTimeout callback to stack
console.log("2")executes
Microtasks vs Macrotasks
Microtasks (higher priority):
- Promises (.then, .catch, .finally)
- async/await
- queueMicrotask()
Macrotasks (lower priority):
- setTimeout
- setInterval
- setImmediate (Node.js)
- I/O operations
Example:
console.log("1");
setTimeout(() => console.log("2"), 0); // Macrotask
Promise.resolve().then(() => console.log("3")); // Microtask
console.log("4");
// Output:
// 1
// 4
// 3 (microtask runs before macrotask)
// 2
7. Practical Examples
Example 1: Simulated API with Delay
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchWithDelay(url) {
console.log(`Fetching ${url}...`);
await delay(1000); // Simulate network delay
return { data: `Data from ${url}` };
}
async function getData() {
const result = await fetchWithDelay("https://api.example.com");
console.log(result.data);
}
getData();
Example 2: Retry Logic
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (response.ok) {
return await response.json();
}
} catch (error) {
console.log(`Attempt ${i + 1} failed`);
if (i === retries - 1) throw error;
}
}
}
fetchWithRetry("https://api.example.com/data")
.then(data => console.log(data))
.catch(error => console.log("All retries failed"));
Example 3: Loading Multiple Resources
async function loadDashboard() {
try {
const [user, posts, comments] = await Promise.all([
fetch("/api/user").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json())
]);
console.log("Dashboard loaded:", { user, posts, comments });
} catch (error) {
console.log("Failed to load dashboard:", error);
}
}
8. Practical Exercises
Exercise 4.7.1: Promise Practice
Create a function that:
- Returns a promise
- Resolves after random delay (500-2000ms)
- Randomly succeeds or fails (50/50)
- Handle with .then/.catch
Exercise 4.7.2: Async/Await Conversion
Convert this promise chain to async/await:
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => console.log(comments))
.catch(error => console.log(error));
Exercise 4.7.3: Parallel Execution
Fetch data from 5 URLs simultaneously:
- Use Promise.all()
- Log results when all complete
- Handle errors gracefully
- Show loading indicator
Exercise 4.7.4: Race Condition
Create a timeout function that:
- Races between actual fetch and timeout
- Rejects if timeout wins
- Resolves with data if fetch wins
9. Knowledge Check
Question 1: What's the difference between synchronous and asynchronous code?
Show answer
Synchronous code executes line by line, blocking until each operation completes. Asynchronous code allows operations to run in background, not blocking execution.Question 2: What are the three states of a promise?
Show answer
Pending (initial), Fulfilled (success), Rejected (failure).Question 3: What does async/await do?
Show answer
Syntactic sugar over promises. `async` makes function return promise. `await` pauses execution until promise resolves, making async code look synchronous.Question 4: What's the difference between Promise.all() and Promise.race()?
Show answer
Promise.all() waits for ALL promises to resolve. Promise.race() returns first promise to settle (resolve or reject).Question 5: Why does setTimeout with 0ms still run after synchronous code?
Show answer
Event loop only processes callback queue after call stack is empty. Even 0ms setTimeout goes to Web API first, then callback queue.10. Key Takeaways
- JavaScript is single-threaded but handles async via event loop
- Callbacks are functions passed as arguments, executed later
- Promises represent eventual result of async operation (pending/fulfilled/rejected)
- async/await is modern syntax for promises (cleaner, more readable)
awaitonly works insideasyncfunctions- Use try/catch for error handling in async/await
- Promise.all() for parallel execution (all must succeed)
- Promise.race() for first to complete
- Microtasks (promises) run before macrotasks (setTimeout)
- Always handle errors in async code
11. Further Resources
Documentation:
Next Steps
Outstanding! You now understand asynchronous JavaScript and can write clean async code.
In Lesson 4.8: Fetch API & AJAX, you'll learn to make HTTP requests to fetch data from servers and build data-driven applications.
Next: Lesson 4.8 - Fetch API & AJAX ā