Lesson 2.3: then, catch, finally
Duration: 60 minutes
Learning Objectives
By the end of this lesson, you will be able to:
- Consume Promises using
.then()to handle success - Handle errors using
.catch() - Execute cleanup code with
.finally() - Chain multiple
.then()calls for sequential operations - Understand how values flow through a Promise chain
Consuming Promises with .then()
The .then() method registers a callback to be called when the Promise is fulfilled:
const promise = Promise.resolve('Hello, World!');
promise.then((value) => {
console.log(value); // "Hello, World!"
});
Basic Syntax
promise.then(onFulfilled, onRejected);
// onFulfilled: Called when Promise resolves
// onRejected: Optional, called when Promise rejects
Example with Async Operation
function fetchUser(id: number): Promise<{ id: number; name: string }> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id, name: 'Alice' });
}, 1000);
});
}
fetchUser(1).then((user) => {
console.log(`User: ${user.name}`); // "User: Alice" (after 1 second)
});
console.log('Fetching user...'); // This runs first!
Output:
Fetching user...
User: Alice
Handling Errors with .catch()
The .catch() method handles rejected Promises:
const promise = Promise.reject(new Error('Something went wrong'));
promise.catch((error) => {
console.log('Error:', error.message); // "Error: Something went wrong"
});
Why Use .catch()?
You could handle errors in .then():
promise.then(
(value) => console.log('Success:', value),
(error) => console.log('Error:', error.message)
);
But .catch() is cleaner and catches errors from the entire chain:
fetchUser(1)
.then((user) => {
throw new Error('Processing failed!'); // This error...
})
.catch((error) => {
console.log('Caught:', error.message); // ...is caught here!
});
Practical Example
function fetchData(url: string): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === '') {
reject(new Error('URL cannot be empty'));
return;
}
if (url.includes('error')) {
reject(new Error('Server error'));
return;
}
resolve(`Data from ${url}`);
}, 500);
});
}
// Success case
fetchData('https://api.example.com/users')
.then((data) => console.log('Data:', data))
.catch((error) => console.log('Error:', error.message));
// Output: "Data: Data from https://api.example.com/users"
// Error case
fetchData('https://api.example.com/error')
.then((data) => console.log('Data:', data))
.catch((error) => console.log('Error:', error.message));
// Output: "Error: Server error"
Cleanup with .finally()
The .finally() method runs regardless of whether the Promise was fulfilled or rejected:
function loadData(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve('Data loaded'), 1000);
});
}
console.log('Loading...');
loadData()
.then((data) => console.log('Success:', data))
.catch((error) => console.log('Error:', error.message))
.finally(() => console.log('Cleanup complete'));
Common Use Cases for .finally()
- Hide loading indicators:
let isLoading = true;
fetchData()
.then((data) => displayData(data))
.catch((error) => showError(error))
.finally(() => {
isLoading = false; // Always runs
hideSpinner();
});
- Close connections:
openDatabase()
.then((db) => db.query('SELECT * FROM users'))
.then((users) => processUsers(users))
.catch((error) => logError(error))
.finally(() => closeDatabase()); // Always close
- Reset state:
let buttonDisabled = true;
submitForm()
.then((result) => showSuccess(result))
.catch((error) => showError(error))
.finally(() => {
buttonDisabled = false; // Re-enable button
});
.finally() Does Not Receive Arguments
Promise.resolve('Hello')
.finally(() => {
// No access to the resolved value here
console.log('Cleaning up');
})
.then((value) => {
// But the value passes through!
console.log(value); // "Hello"
});
Promise Chaining
The real power of Promises is chaining. Each .then() returns a new Promise, allowing you to chain operations:
Promise.resolve(1)
.then((value) => {
console.log(value); // 1
return value + 1;
})
.then((value) => {
console.log(value); // 2
return value * 2;
})
.then((value) => {
console.log(value); // 4
});
How Values Flow Through the Chain
Promise.resolve(1)
│
▼ value = 1
.then(v => v + 1) → Returns Promise<2>
│
▼ value = 2
.then(v => v * 2) → Returns Promise<4>
│
▼ value = 4
.then(v => console.log(v))
Returning Promises in .then()
When you return a Promise from .then(), the chain waits for it to resolve:
function step1(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => resolve(10), 500);
});
}
function step2(value: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => resolve(value * 2), 500);
});
}
function step3(value: number): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve(`Result: ${value}`), 500);
});
}
step1()
.then((result) => step2(result)) // Waits for step2
.then((result) => step3(result)) // Waits for step3
.then((result) => console.log(result)); // "Result: 20" (after 1.5 seconds)
Practical Chaining Example
interface User {
id: number;
name: string;
}
interface Post {
id: number;
userId: number;
title: string;
}
interface Comment {
id: number;
postId: number;
body: string;
}
function fetchUser(id: number): Promise<User> {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: 'Alice' }), 300);
});
}
function fetchPosts(userId: number): Promise<Post[]> {
return new Promise((resolve) => {
setTimeout(
() =>
resolve([
{ id: 1, userId, title: 'First Post' },
{ id: 2, userId, title: 'Second Post' },
]),
300
);
});
}
function fetchComments(postId: number): Promise<Comment[]> {
return new Promise((resolve) => {
setTimeout(
() =>
resolve([
{ id: 1, postId, body: 'Great post!' },
{ id: 2, postId, body: 'Thanks for sharing' },
]),
300
);
});
}
// Chained operations - clean and readable!
fetchUser(1)
.then((user) => {
console.log(`User: ${user.name}`);
return fetchPosts(user.id);
})
.then((posts) => {
console.log(`Posts: ${posts.length}`);
return fetchComments(posts[0].id);
})
.then((comments) => {
console.log(`Comments: ${comments.length}`);
})
.catch((error) => {
console.error('Something went wrong:', error.message);
});
Compare this to the callback hell version - much cleaner!
Error Propagation in Chains
Errors propagate down the chain until they are caught:
Promise.resolve(1)
.then((value) => {
throw new Error('Error in step 1');
return value + 1; // Never reached
})
.then((value) => {
console.log('Step 2:', value); // Never reached
return value * 2;
})
.then((value) => {
console.log('Step 3:', value); // Never reached
})
.catch((error) => {
console.log('Caught:', error.message); // "Caught: Error in step 1"
});
Error Flow Diagram
.then(step1) ──── Error! ────┐
│ │
│ │
.then(step2) ← skipped ←─────┤
│ │
│ │
.then(step3) ← skipped ←─────┤
│ │
│ │
.catch(handler) ←─────────────┘
Recovering from Errors
You can recover from errors by catching them mid-chain:
Promise.resolve(1)
.then((value) => {
throw new Error('Oops!');
})
.catch((error) => {
console.log('Recovering from:', error.message);
return 0; // Recovery value
})
.then((value) => {
console.log('Recovered with:', value); // "Recovered with: 0"
});
Multiple .catch() Handlers
fetchUser(1)
.then((user) => {
if (!user.name) {
throw new Error('Invalid user');
}
return fetchPosts(user.id);
})
.catch((error) => {
// Handle user-related errors
console.log('User error:', error.message);
return []; // Return empty posts array
})
.then((posts) => {
console.log('Posts count:', posts.length);
return posts;
})
.catch((error) => {
// Handle any remaining errors
console.log('Unexpected error:', error.message);
});
Common Patterns
Pattern 1: Transform Data
interface ApiUser {
user_id: number;
full_name: string;
email_address: string;
}
interface User {
id: number;
name: string;
email: string;
}
function fetchApiUser(): Promise<ApiUser> {
return Promise.resolve({
user_id: 1,
full_name: 'Alice Smith',
email_address: 'alice@example.com',
});
}
fetchApiUser()
.then(
(apiUser): User => ({
id: apiUser.user_id,
name: apiUser.full_name,
email: apiUser.email_address,
})
)
.then((user) => {
console.log(user);
// { id: 1, name: "Alice Smith", email: "alice@example.com" }
});
Pattern 2: Conditional Branching
function fetchData(useCache: boolean): Promise<string> {
if (useCache) {
return Promise.resolve('Cached data');
}
return fetchFromServer();
}
fetchData(true)
.then((data) => console.log('Data:', data))
.catch((error) => console.log('Error:', error.message));
Pattern 3: Retry Logic
function fetchWithRetry(url: string, retries: number = 3): Promise<string> {
return fetch(url).catch((error) => {
if (retries > 0) {
console.log(`Retrying... (${retries} attempts left)`);
return fetchWithRetry(url, retries - 1);
}
throw error;
});
}
Pattern 4: Timeout Handling
function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms);
});
return Promise.race([promise, timeoutPromise]);
}
// Usage
timeout(fetchUser(1), 5000)
.then((user) => console.log('User:', user))
.catch((error) => console.log('Error:', error.message));
Common Mistakes
Mistake 1: Forgetting to Return
// Wrong - the chain breaks
fetchUser(1)
.then((user) => {
fetchPosts(user.id); // Forgot return!
})
.then((posts) => {
console.log(posts); // undefined!
});
// Correct
fetchUser(1)
.then((user) => {
return fetchPosts(user.id); // Return the Promise
})
.then((posts) => {
console.log(posts); // Posts array
});
Mistake 2: Nesting Instead of Chaining
// Wrong - callback hell with Promises!
fetchUser(1).then((user) => {
fetchPosts(user.id).then((posts) => {
fetchComments(posts[0].id).then((comments) => {
console.log(comments);
});
});
});
// Correct - flat chain
fetchUser(1)
.then((user) => fetchPosts(user.id))
.then((posts) => fetchComments(posts[0].id))
.then((comments) => console.log(comments));
Mistake 3: Missing Error Handler
// Dangerous - unhandled rejection
fetchUser(1).then((user) => {
throw new Error('Oops!');
});
// Always add .catch()
fetchUser(1)
.then((user) => {
throw new Error('Oops!');
})
.catch((error) => {
console.error('Handled:', error.message);
});
Mistake 4: Catching Too Early
// Problem - error is swallowed
fetchUser(1)
.catch((error) => {
console.log('Error:', error.message);
// Returns undefined, chain continues
})
.then((user) => {
console.log(user); // undefined if error occurred
});
// Better - rethrow if needed
fetchUser(1)
.catch((error) => {
console.log('Logging error:', error.message);
throw error; // Rethrow for downstream handling
})
.then((user) => {
console.log(user);
})
.catch((error) => {
console.log('Final handler:', error.message);
});
Exercises
Exercise 1: Basic Chain
Create a Promise chain that:
- Starts with the number 5
- Doubles it
- Adds 10
- Logs the final result
Solution
Promise.resolve(5)
.then((value) => value * 2) // 10
.then((value) => value + 10) // 20
.then((value) => console.log('Result:', value)); // "Result: 20"
Exercise 2: Error Handling
Fix this code so errors are properly handled:
function riskyOperation(value: number): Promise<number> {
return new Promise((resolve, reject) => {
if (value < 0) {
reject(new Error('Value must be positive'));
} else {
resolve(value * 2);
}
});
}
// Fix this chain
riskyOperation(-5).then((result) => console.log('Result:', result));
Solution
riskyOperation(-5)
.then((result) => console.log('Result:', result))
.catch((error) => console.log('Error:', error.message));
// Output: "Error: Value must be positive"
Exercise 3: Chain with .finally()
Create a chain that:
- Simulates a data fetch (resolve after 500ms with "Data loaded")
- Logs the data
- Has error handling
- Always logs "Operation complete" at the end
Solution
function fetchData(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => resolve('Data loaded'), 500);
});
}
fetchData()
.then((data) => console.log('Success:', data))
.catch((error) => console.log('Error:', error.message))
.finally(() => console.log('Operation complete'));
// Output:
// Success: Data loaded
// Operation complete
Exercise 4: Sequential Operations
Create functions and chain them to:
- Get a user by ID (return
{ id, name }) - Get the user's settings (return
{ theme: "dark", language: "en" }) - Log both the user and settings
Each step should take 300ms.
Solution
interface User {
id: number;
name: string;
}
interface Settings {
theme: string;
language: string;
}
function getUser(id: number): Promise<User> {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: 'Alice' }), 300);
});
}
function getSettings(userId: number): Promise<Settings> {
return new Promise((resolve) => {
setTimeout(() => resolve({ theme: 'dark', language: 'en' }), 300);
});
}
let currentUser: User;
getUser(1)
.then((user) => {
currentUser = user;
console.log('User:', user.name);
return getSettings(user.id);
})
.then((settings) => {
console.log('Settings:', settings);
console.log(`${currentUser.name} uses ${settings.theme} theme`);
})
.catch((error) => console.log('Error:', error.message));
Exercise 5: Error Recovery
Create a chain that:
- Attempts to fetch data (rejects with "Network error")
- Catches the error and returns a default value "Default data"
- Processes the data (either fetched or default)
- Logs the final result
Solution
function fetchData(): Promise<string> {
return Promise.reject(new Error('Network error'));
}
function processData(data: string): string {
return data.toUpperCase();
}
fetchData()
.catch((error) => {
console.log('Fetch failed:', error.message);
return 'Default data'; // Recovery value
})
.then((data) => processData(data))
.then((processed) => console.log('Result:', processed));
// Output:
// Fetch failed: Network error
// Result: DEFAULT DATA
Key Takeaways
.then()handles successful Promise resolution.catch()handles errors and can recover with a return value.finally()runs regardless of success/failure for cleanup- Chaining allows sequential async operations without nesting
- Return values from
.then()become the input for the next.then() - Returning a Promise in
.then()waits for it before continuing - Errors propagate down the chain until caught
- Always include error handling in your Promise chains
Resources
| Resource | Type | Description |
|---|---|---|
| MDN: Promise.prototype.then() | Documentation | Complete then() reference |
| MDN: Promise.prototype.catch() | Documentation | Error handling with catch() |
| JavaScript.info: Promise Chaining | Tutorial | Interactive chaining tutorial |
Next Lesson
Now that you can chain Promises, let us learn how to work with multiple Promises at once using Promise.all() and Promise.race().