How do you handle errors in asynchronous operations?

JavaScript

The short answer

For promises, use .catch() at the end of the chain. For async/await, use try...catch. For multiple parallel operations, use Promise.allSettled to handle failures individually. In React, use error boundaries for rendering errors and try...catch inside useEffect for async errors.

With promise chains

fetch('/api/users')
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.log('Something went wrong:', error.message);
});

A single .catch() at the end handles errors from any step in the chain. If the network fails, the response is not OK, or the JSON parsing fails — all of these end up in the .catch().

With async/await

async function getUsers() {
try {
const response = await fetch('/api/users');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.log('Failed to fetch users:', error.message);
return [];
}
}

Handling multiple async operations

Promise.all — fails fast (one failure cancels everything):

try {
const [users, posts] = await Promise.all([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
]);
} catch (error) {
// If either request fails, this catches it
console.log('One of the requests failed:', error.message);
}

Promise.allSettled — get results from all, even if some fail:

const results = await Promise.allSettled([
fetch('/api/users').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
]);
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failed:', result.reason);
}
});

Promise.allSettled never rejects. Each result has a status of either 'fulfilled' or 'rejected'.

In React components

function UserList() {
const [users, setUsers] = useState([]);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
fetch('/api/users', { signal: controller.signal })
.then((response) => {
if (!response.ok)
throw new Error('Failed to fetch');
return response.json();
})
.then((data) => {
setUsers(data);
setLoading(false);
})
.catch((error) => {
if (error.name !== 'AbortError') {
setError(error.message);
setLoading(false);
}
});
return () => controller.abort();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}

Common Pitfalls

A common mistake is not checking response.ok after a fetch call. fetch only rejects on network errors (like no internet). A 404 or 500 response does not throw — it resolves with response.ok set to false. You must check this manually and throw if the response is not OK.

Interview Tip

Cover three patterns: .catch() with promises, try...catch with async/await, and Promise.allSettled for parallel operations. The fetch gotcha about response.ok is a great thing to mention because many candidates do not know about it. If the interview is React-focused, show the loading/error/data state pattern.

Why interviewers ask this

Error handling in async code is where real bugs happen. Network requests fail, APIs return errors, and timeouts occur. Interviewers want to see if you handle these cases gracefully instead of letting the app crash. A candidate who knows about response.ok, Promise.allSettled, and React error states shows they build reliable applications.