How to Handle Errors with Promises in JavaScript
When asynchronous operations fail, you need a reliable way to catch and respond to those failures. Promises provide a structured error handling system that is fundamentally different from the synchronous try...catch approach. Every .then() handler has an invisible try...catch around it. Errors propagate automatically down the chain until they find a .catch(). Unhandled rejections trigger global events that can crash your application in Node.js or flood your console in the browser.
Understanding how errors flow through promise chains is critical for writing robust asynchronous code. A misplaced .catch(), a missing return statement, or a swallowed rejection can create bugs that are incredibly difficult to track down because they fail silently.
This guide covers exactly how promise error handling works, where errors go when they are thrown, how to position your .catch() handlers effectively, and the common mistakes that trip up even experienced developers.
Implicit try...catch in Promise Handlers
Every function you pass to .then(), .catch(), or .finally() is wrapped in an invisible try...catch by the promise machinery. If the function throws an error (any error, for any reason), the promise returned by that handler is automatically rejected with that error. The error does not crash your program or escape into the void. It becomes a rejection that flows through the chain.
Errors in the Executor Function
The same invisible try...catch wraps the executor function you pass to new Promise():
const promise = new Promise((resolve, reject) => {
// This throw is automatically caught by the Promise constructor
throw new Error("Something broke in the executor");
});
promise.catch(err => {
console.log(err.message); // "Something broke in the executor"
});
This is exactly equivalent to explicitly calling reject():
const promise = new Promise((resolve, reject) => {
reject(new Error("Something broke in the executor"));
});
promise.catch(err => {
console.log(err.message); // "Something broke in the executor"
});
Both produce the same result: a rejected promise. The implicit try...catch means you do not need to wrap your executor code in your own try...catch just to call reject() on failure.
Errors in .then() Handlers
The same applies inside .then() callbacks. Any error thrown becomes a rejection of the promise returned by .then():
Promise.resolve("hello")
.then(value => {
// This error is caught by the implicit try...catch
throw new Error("Failed in .then()");
})
.catch(err => {
console.log(err.message); // "Failed in .then()"
});
This works for all types of runtime errors, not just manually thrown ones:
Promise.resolve(null)
.then(value => {
// TypeError: Cannot read properties of null
return value.toUpperCase();
})
.catch(err => {
console.log(err.name); // "TypeError"
console.log(err.message); // "Cannot read properties of null (reading 'toUpperCase')"
});
Promise.resolve("data")
.then(value => {
// ReferenceError: undeclaredVariable is not defined
return undeclaredVariable + value;
})
.catch(err => {
console.log(err.name); // "ReferenceError"
});
What the Implicit try...catch Does NOT Cover
The implicit wrapping only applies to synchronous errors inside the handler. If you start an asynchronous operation inside a handler without returning its promise, errors from that async operation are not caught:
Promise.resolve()
.then(() => {
// This setTimeout callback runs OUTSIDE the promise chain
setTimeout(() => {
throw new Error("This is NOT caught by .catch()");
}, 100);
})
.catch(err => {
// This never runs for the setTimeout error
console.log("Caught:", err.message);
});
The setTimeout callback runs later, outside the promise machinery. Its error has no promise chain to flow through. It becomes an unhandled exception in the global scope.
The implicit try...catch only catches errors thrown synchronously inside the handler function, or rejections from promises that are returned from the handler. Asynchronous errors from callbacks (setTimeout, event handlers) that are not tied to a returned promise will escape the chain.
Comparison with Synchronous try...catch
// Synchronous: you write your own try...catch
try {
JSON.parse("{bad json}");
} catch (err) {
console.log("Caught:", err.message);
}
// Promise: the try...catch is built in
Promise.resolve("{bad json}")
.then(str => JSON.parse(str)) // Error thrown here...
.catch(err => { // ...is caught here automatically
console.log("Caught:", err.message);
});
Error Propagation Through the Chain
One of the most powerful features of promise error handling is automatic propagation. When an error occurs (either a rejection or a thrown error), it skips all subsequent .then() handlers and jumps to the nearest .catch() further down the chain.
Errors Skip .then() and Find .catch()
fetch("https://invalid-url.example.com/data")
.then(response => {
console.log("Step 1: Got response"); // Skipped
return response.json();
})
.then(data => {
console.log("Step 2: Parsed data"); // Skipped
return processData(data);
})
.then(result => {
console.log("Step 3: Processed"); // Skipped
return saveResult(result);
})
.catch(err => {
// The fetch error jumps directly here
console.log("Error caught:", err.message);
});
When fetch rejects (network error, DNS failure), all three .then() handlers are completely bypassed. Execution jumps straight to .catch(). This is analogous to how a thrown error in synchronous code skips all lines after it and jumps to the catch block.
Visualizing the Flow
Think of a promise chain as a railroad track with two paths: a success track and an error track.
.then(handler1) → .then(handler2) → .then(handler3) → .catch(errorHandler)
↓ error ↓ error ↓ error ↑
└─────────────────┴──────────────────┴──────────────────────┘
error track
An error at any point switches to the error track. The error slides along the error track, skipping all .then() handlers, until it reaches a .catch().
Errors Can Originate at Any Point
The propagation works regardless of where in the chain the error occurs:
// Error in step 1
Promise.resolve()
.then(() => { throw new Error("Step 1 failed"); })
.then(() => console.log("Step 2")) // Skipped
.then(() => console.log("Step 3")) // Skipped
.catch(err => console.log("Caught:", err.message));
// "Caught: Step 1 failed"
// Error in step 2
Promise.resolve()
.then(() => console.log("Step 1")) // Runs
.then(() => { throw new Error("Step 2 failed"); })
.then(() => console.log("Step 3")) // Skipped
.catch(err => console.log("Caught:", err.message));
// "Step 1"
// "Caught: Step 2 failed"
// Error in step 3
Promise.resolve()
.then(() => console.log("Step 1")) // Runs
.then(() => console.log("Step 2")) // Runs
.then(() => { throw new Error("Step 3 failed"); })
.catch(err => console.log("Caught:", err.message));
// "Step 1"
// "Step 2"
// "Caught: Step 3 failed"
Rejections from Returned Promises Also Propagate
When a .then() handler returns a promise that rejects, the rejection propagates the same way:
function fetchUser(id) {
return fetch(`https://api.example.com/users/${id}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
});
}
function fetchUserPosts(userId) {
return fetch(`https://api.example.com/users/${userId}/posts`)
.then(response => response.json());
}
fetchUser(1)
.then(user => {
console.log("Got user:", user.name);
return fetchUserPosts(user.id); // Returns a promise that might reject
})
.then(posts => {
console.log("Got posts:", posts.length);
})
.catch(err => {
// Catches errors from fetchUser OR fetchUserPosts
console.log("Something failed:", err.message);
});
The .catch() at the end handles rejections from any point in the chain, whether from fetchUser, fetchUserPosts, or any of the .then() handlers.
Unhandled Promise Rejections
When a promise rejects and there is no .catch() anywhere in its chain, the rejection is unhandled. This is a serious problem. In modern environments, unhandled rejections trigger warning events and can even terminate your process.
What Happens with No .catch()
// This rejection has nowhere to go
Promise.reject(new Error("Nobody is listening"));
// This chain has no error handler
fetch("https://invalid.example.com")
.then(response => response.json())
.then(data => console.log(data));
// No .catch(): any error is unhandled
Browser Behavior
Browsers fire an unhandledrejection event on the window object:
window.addEventListener("unhandledrejection", event => {
console.warn("Unhandled rejection:", event.reason);
console.warn("Promise:", event.promise);
// Optionally prevent the default console error
event.preventDefault();
});
// This rejection triggers the event
Promise.reject(new Error("Unhandled!"));
Without the event listener, you see an error in the console like:
Uncaught (in promise) Error: Unhandled!
Node.js Behavior
Node.js has become increasingly strict about unhandled rejections. In modern versions, an unhandled rejection terminates the process by default:
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection:", reason);
// In production, log and exit gracefully
process.exit(1);
});
The rejectionhandled Event
If a rejection is initially unhandled but a .catch() is attached later, browsers fire a rejectionhandled event:
window.addEventListener("unhandledrejection", event => {
console.log("Unhandled rejection detected");
});
window.addEventListener("rejectionhandled", event => {
console.log("Rejection was handled later");
});
const p = Promise.reject(new Error("delayed handling"));
// Handler attached later
setTimeout(() => {
p.catch(err => console.log("Now handled:", err.message));
}, 1000);
// "Unhandled rejection detected" (immediately)
// (1 second later)
// "Rejection was handled later"
// "Now handled: delayed handling"
Never rely on attaching .catch() later. Always attach error handlers synchronously when constructing your promise chains. Treat every unhandled rejection as a bug.
Preventing Unhandled Rejections in Practice
// BAD: no error handling
async function loadData() {
const response = await fetch("/api/data");
const data = await response.json();
displayData(data);
}
loadData(); // If fetch fails, unhandled rejection!
// GOOD: catch at the call site
loadData().catch(err => {
console.error("Failed to load data:", err);
displayError("Could not load data");
});
// ALSO GOOD: catch inside the function
async function loadDataSafe() {
try {
const response = await fetch("/api/data");
const data = await response.json();
displayData(data);
} catch (err) {
console.error("Failed to load data:", err);
displayError("Could not load data");
}
}
loadDataSafe(); // Error is handled internally
.catch() Placement: Where in the Chain?
Where you place .catch() dramatically affects which errors are handled and what happens after an error. This is one of the most important decisions in promise chain design.
.catch() at the End: Catch-All
The most common pattern. A single .catch() at the end handles errors from any point in the chain:
fetchUser(userId)
.then(user => fetchUserOrders(user.id))
.then(orders => calculateTotal(orders))
.then(total => displayTotal(total))
.catch(err => {
// Handles errors from ANY of the above steps
console.error("Pipeline failed:", err.message);
displayError("Could not load order total");
});
This is clean and simple, but it has a limitation: you cannot distinguish where the error came from without inspecting the error object.
.catch() in the Middle: Recovery
A .catch() in the middle of a chain can handle an error and allow the chain to continue. The promise returned by .catch() is resolved (not rejected) if the .catch() handler does not throw:
fetchUserAvatar(userId)
.catch(err => {
// If avatar fetch fails, use a default (chain continues)
console.warn("Avatar failed, using default:", err.message);
return "/images/default-avatar.png";
})
.then(avatarUrl => {
// This runs with either the real avatar or the default
console.log("Using avatar:", avatarUrl);
displayAvatar(avatarUrl);
});
The key insight: after .catch() handles an error (without throwing or returning a rejected promise), the chain switches back to the success track.
Middle .catch() with Continued Error Handling
fetch("/api/primary-data")
.then(response => response.json())
.catch(err => {
// Primary source failed (try fallback)
console.warn("Primary failed, trying fallback:", err.message);
return fetch("/api/fallback-data").then(r => r.json());
})
.then(data => {
// Works with data from primary or fallback
console.log("Got data:", data);
return processData(data);
})
.catch(err => {
// Both primary AND fallback failed
console.error("All sources failed:", err.message);
});
Multiple .catch() Blocks for Different Stages
function getUserDashboard(userId) {
return fetchUser(userId)
.catch(err => {
// User fetch is critical (rethrow with context)
throw new Error(`Cannot load dashboard: user fetch failed (${err.message})`);
})
.then(user => {
return fetchUserPreferences(user.id)
.catch(err => {
// Preferences are optional (use defaults)
console.warn("Using default preferences:", err.message);
return { theme: "light", language: "en" };
})
.then(prefs => ({ user, prefs }));
})
.then(({ user, prefs }) => {
return fetchNotifications(user.id)
.catch(err => {
// Notifications are optional (show empty)
console.warn("Notifications unavailable:", err.message);
return [];
})
.then(notifications => ({ user, prefs, notifications }));
});
}
getUserDashboard(42)
.then(dashboard => {
console.log("Dashboard loaded:", dashboard);
// dashboard.user is guaranteed
// dashboard.prefs has real or default values
// dashboard.notifications has real or empty array
})
.catch(err => {
// Only reaches here if the critical user fetch failed
console.error("Dashboard failed:", err.message);
});
The Trap: .catch() Before .then() Does Not Catch .then() Errors
Promise.resolve("data")
.catch(err => {
// This only catches errors from ABOVE (the resolve)
// It does NOT catch errors from the .then() BELOW
console.log("Caught:", err.message);
})
.then(value => {
// If this throws, there's no .catch() after it!
throw new Error("Error in .then()");
});
// Unhandled rejection: Error in .then()
Each .catch() only handles errors from the chain above it, not below it.
Visualizing .catch() Placement
Scenario 1: .catch() at the end
.then(A) → .then(B) → .then(C) → .catch(handler)
↓ err ↓ err ↓ err ↑
└───────────┴───────────┴─────────────┘
Catches errors from A, B, or C
Scenario 2: .catch() in the middle
.then(A) → .catch(recovery) → .then(B) → .then(C)
↓ err ↑
└─────────────┘
A fails → recovery runs → B and C continue on success track
If recovery throws → B and C are skipped → UNHANDLED (no final .catch)
Scenario 3: Multiple .catch() blocks
.then(A) → .catch(handle_A) → .then(B) → .catch(handle_B)
↓ err ↑ ↓ err ↑
└────────────┘ └────────────┘
A errors caught by handle_A, B errors caught by handle_B
Rethrowing in Promise Chains
Just like with synchronous try...catch, you often want to catch an error, inspect it, handle the ones you understand, and rethrow the ones you do not. In promise chains, rethrowing means throwing inside a .catch() handler (or returning a rejected promise), which puts the error back on the error track.
Basic Rethrowing
fetch("/api/data")
.then(response => {
if (response.status === 404) {
throw new Error("NOT_FOUND");
}
if (response.status === 401) {
throw new Error("UNAUTHORIZED");
}
if (!response.ok) {
throw new Error(`HTTP_ERROR_${response.status}`);
}
return response.json();
})
.catch(err => {
if (err.message === "NOT_FOUND") {
console.log("Resource not found - showing empty state");
return { items: [] }; // Recover with empty data
}
if (err.message === "UNAUTHORIZED") {
console.log("Not authorized - redirecting to login");
redirectToLogin();
return; // Chain stops here (returns undefined, which is fine)
}
// Unknown error: rethrow it
throw err;
})
.then(data => {
// Only runs if .catch() recovered (NOT_FOUND case)
// Does not run if .catch() rethrew
console.log("Data:", data);
})
.catch(err => {
// Catches rethrown errors
console.error("Unrecoverable error:", err.message);
});
Rethrowing with Enhanced Context
A common pattern is to catch an error, add context, and rethrow:
function loadUserProfile(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(err => {
// Add context and rethrow
throw new Error(`Failed to load profile for user ${userId}: ${err.message}`);
});
}
function loadUserPosts(userId) {
return fetch(`/api/users/${userId}/posts`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(err => {
throw new Error(`Failed to load posts for user ${userId}: ${err.message}`);
});
}
loadUserProfile(42)
.then(user => {
console.log("User:", user.name);
return loadUserPosts(user.id);
})
.then(posts => {
console.log("Posts:", posts.length);
})
.catch(err => {
// The error message tells us exactly what failed
console.error(err.message);
// "Failed to load profile for user 42: HTTP 500"
// or
// "Failed to load posts for user 42: HTTP 404"
});
Rethrowing: .catch() Returns a Rejected Promise
There are two ways to rethrow from .catch():
// Way 1: throw (most common)
.catch(err => {
console.log("Logged:", err.message);
throw err; // Rethrows the same error
})
// Way 2: return Promise.reject()
.catch(err => {
console.log("Logged:", err.message);
return Promise.reject(err); // Same effect
})
// Way 3: return a new rejected promise with a new error
.catch(err => {
return Promise.reject(new Error(`Wrapped: ${err.message}`));
})
All three put the error back on the error track, causing subsequent .then() handlers to be skipped and the next .catch() to fire.
Selective Handling Pattern
class HttpError extends Error {
constructor(status, message) {
super(message);
this.name = "HttpError";
this.status = status;
}
}
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = "NetworkError";
}
}
function fetchJSON(url) {
return fetch(url)
.catch(err => {
// fetch rejects on network errors only
throw new NetworkError(`Network error fetching ${url}: ${err.message}`);
})
.then(response => {
if (!response.ok) {
throw new HttpError(response.status, `${url} returned ${response.status}`);
}
return response.json();
});
}
fetchJSON("/api/data")
.catch(err => {
if (err instanceof NetworkError) {
console.log("Network problem - check your connection");
return getCachedData(); // Recover with cached data
}
if (err instanceof HttpError && err.status === 404) {
console.log("Not found - returning empty result");
return { items: [] }; // Recover with empty data
}
// All other errors: rethrow
throw err;
})
.then(data => {
console.log("Working with data:", data);
})
.catch(err => {
// Final catch for unrecoverable errors
console.error("Fatal:", err.message);
showErrorPage();
});
Common Mistake: Forgetting to Return in .then()
This is the single most common and insidious mistake in promise chain programming. When you call an asynchronous function inside .then() but forget to return its promise, the chain does not wait for it. The inner promise runs independently, its errors are unhandled, and the outer chain continues with undefined.
The Problem
// BROKEN: missing return
fetch("/api/users/1")
.then(response => response.json())
.then(user => {
// This fetch runs, but its promise is NOT returned
fetch(`/api/users/${user.id}/posts`)
.then(response => response.json())
.then(posts => {
console.log("Posts:", posts);
});
// Implicitly returns undefined
})
.then(result => {
console.log("Result:", result); // undefined (didn't wait for posts!)
})
.catch(err => {
// Does NOT catch errors from the inner fetch!
console.error("Error:", err);
});
What happens step by step:
- Fetch user succeeds
- Inner
fetchfor posts starts but its promise is not returned - The outer
.then()returnsundefinedimmediately - The next
.then()runs withresult = undefinedbefore posts arrive - If the inner fetch fails, the error is unhandled because it is not connected to the outer chain
The Fix: Always Return Promises
// CORRECT: return the inner promise
fetch("/api/users/1")
.then(response => response.json())
.then(user => {
// Return the promise so the chain waits for it
return fetch(`/api/users/${user.id}/posts`)
.then(response => response.json());
})
.then(posts => {
console.log("Posts:", posts); // Actual posts data
})
.catch(err => {
// Catches errors from BOTH fetches
console.error("Error:", err);
});
Spotting the Missing Return
Here are variations of this mistake and their fixes:
// BROKEN: fetch inside .then() not returned
.then(user => {
fetch(`/api/posts/${user.id}`); // Fire-and-forget!
})
// FIXED
.then(user => {
return fetch(`/api/posts/${user.id}`);
})
// ALSO FIXED: arrow function with implicit return
.then(user => fetch(`/api/posts/${user.id}`))
// BROKEN: conditional return
.then(user => {
if (user.isAdmin) {
return fetchAdminData(user.id); // Returned only sometimes
}
// Implicit return undefined when user is not admin
})
// FIXED
.then(user => {
if (user.isAdmin) {
return fetchAdminData(user.id);
}
return fetchRegularData(user.id); // Always return something
})
// BROKEN: promise inside a loop not aggregated
.then(users => {
users.forEach(user => {
fetch(`/api/notify/${user.id}`); // None of these are returned!
});
})
// FIXED: use Promise.all to wait for all operations
.then(users => {
const notifications = users.map(user =>
fetch(`/api/notify/${user.id}`)
);
return Promise.all(notifications);
})
The Arrow Function Shortcut
Single-expression arrow functions have an implicit return, which prevents this mistake:
// Multi-line arrow: needs explicit return
.then(response => {
return response.json(); // Must write 'return'
})
// Single-expression arrow: implicit return
.then(response => response.json()) // Returned automatically
But be careful with arrow functions that use braces:
// BROKEN: braces create a function body, no implicit return
.then(user => {
fetch(`/api/posts/${user.id}`); // Not returned!
})
// FIXED: remove braces for implicit return
.then(user => fetch(`/api/posts/${user.id}`))
// ALSO FIXED: explicit return with braces
.then(user => {
return fetch(`/api/posts/${user.id}`);
})
Testing for Missing Returns
A practical way to detect this bug is to check what the next .then() receives:
somePromise
.then(value => {
doSomethingAsync(value); // Forgot to return
})
.then(result => {
console.log("Result is:", result); // "Result is: undefined"
// If you see undefined when you expected data, trace back to the
// previous .then() and check if it returns a promise
});
When debugging promise chains, add a temporary .then(x => { console.log("checkpoint:", x); return x; }) between steps. If any checkpoint shows undefined when you expected data, the previous handler has a missing return.
fetchUser(1)
.then(user => {
console.log("checkpoint 1:", user); // { id: 1, name: "Alice" }
return fetchPosts(user.id);
})
.then(posts => {
console.log("checkpoint 2:", posts); // undefined? ← missing return above
return formatPosts(posts);
})
.catch(err => console.error(err));
Another Variation: Creating Promises Without Returning Them
// BROKEN: new Promise created but not connected to the chain
function processItem(item) {
return fetch(`/api/items/${item.id}`)
.then(response => response.json())
.then(data => {
// This promise is created but not returned
new Promise((resolve, reject) => {
setTimeout(() => {
saveToCache(data);
resolve();
}, 100);
});
return data; // Returns data, but doesn't wait for cache save
});
}
// FIXED: return the inner promise or chain it
function processItem(item) {
return fetch(`/api/items/${item.id}`)
.then(response => response.json())
.then(data => {
return new Promise((resolve, reject) => {
setTimeout(() => {
saveToCache(data);
resolve(data); // Resolve with data so the chain gets it
}, 100);
});
});
}
Complete Example: Before and After
Here is a realistic example showing a broken chain and its corrected version:
// BROKEN: multiple missing returns and disconnected promises
function loadDashboard(userId) {
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
document.title = user.name;
fetch(`/api/users/${userId}/stats`)
.then(response => response.json())
.then(stats => {
displayStats(stats);
});
fetch(`/api/users/${userId}/notifications`)
.then(response => response.json())
.then(notifications => {
displayNotifications(notifications);
});
})
.catch(err => {
// Only catches user fetch errors
// Stats and notification errors are unhandled!
showError(err.message);
});
// Function returns undefined: caller can't await completion
}
// FIXED: proper returns and error handling
function loadDashboard(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(user => {
document.title = user.name;
// Return Promise.all so the chain waits for both
return Promise.all([
fetch(`/api/users/${userId}/stats`)
.then(r => r.json())
.catch(err => {
console.warn("Stats failed:", err.message);
return null; // Non-critical: recover with null
}),
fetch(`/api/users/${userId}/notifications`)
.then(r => r.json())
.catch(err => {
console.warn("Notifications failed:", err.message);
return []; // Non-critical: recover with empty array
})
]);
})
.then(([stats, notifications]) => {
if (stats) displayStats(stats);
displayNotifications(notifications);
})
.catch(err => {
// Catches user fetch error (critical failure)
showError(err.message);
});
}
// Now the caller can handle completion and errors
loadDashboard(42)
.then(() => console.log("Dashboard loaded"))
.catch(err => console.error("Dashboard failed:", err));
Summary
Promise error handling follows predictable rules, but those rules have subtle implications that catch developers off guard. Understanding exactly how errors propagate, where .catch() blocks intercept them, and what happens when you forget a return is essential for writing reliable asynchronous code.
| Concept | Key Point |
|---|---|
Implicit try...catch | Every .then(), .catch(), and executor function is wrapped in an invisible try...catch. Thrown errors become rejections. |
| Error propagation | Rejections skip all .then() handlers and jump to the nearest .catch() downstream. |
.catch() at the end | Catches errors from any point in the chain. Simplest and most common pattern. |
.catch() in the middle | Can recover from errors and let the chain continue on the success track. |
| Rethrowing | Throwing inside .catch() puts the error back on the error track for the next .catch() to handle. |
| Selective handling | Check error types with instanceof in .catch(), handle known errors, rethrow unknown ones. |
| Unhandled rejections | Promises without .catch() trigger unhandledrejection events. Node.js can terminate the process. |
Missing return | Forgetting to return a promise from .then() disconnects it from the chain. The chain continues with undefined, and inner errors become unhandled. |
| Async errors | The implicit try...catch only covers synchronous code and returned promises, not setTimeout or other non-promise callbacks. |
Key rules to remember:
- Always end every promise chain with
.catch()unless a caller higher up handles errors - Every
.then()handler that creates a promise must return it .catch()only handles errors from above it in the chain, not below- After
.catch()handles an error without rethrowing, the chain switches back to the success track - Use
instanceofchecks in.catch()to selectively handle errors and rethrow the rest - Arrow functions without braces have implicit returns, reducing missing-return bugs
- Treat every
unhandledrejectionwarning as a bug to fix, not noise to ignore