How to Handle Promise Rejections in JavaScript
Learn every technique for handling Promise rejections in JavaScript: .catch(), try/catch with async/await, the unhandledrejection event, rejection chaining, and best practices to prevent silent failures.
Unhandled Promise rejections are one of the most common sources of silent bugs in async JavaScript code. A Promise that rejects without a handler fails invisibly — no error in the console (in older environments), no crash, just incorrect behavior. This guide covers every technique for correctly handling rejections, from basic .catch() to global handlers and advanced patterns.
What Is a Promise Rejection?
A Promise rejects when:
- The executor function calls
reject(reason) - An error is thrown inside the executor
- A
.then()handler throws an error - A
.then()handler returns a rejected Promise
// Rejection via reject()
const p1 = new Promise((resolve, reject) => {
reject(new Error("Something went wrong"));
});
// Rejection via thrown error inside executor
const p2 = new Promise(() => {
throw new Error("Thrown in executor");
});
// Rejection propagated from .then() handler
const p3 = Promise.resolve(1)
.then((n) => {
if (n < 10) throw new Error("Too small");
return n;
});All three produce rejected Promises. The rejection reason is usually (and should always be) an Error object.
Technique 1: .catch() Method
.catch(handler) is the standard way to handle rejections in a Promise chain:
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => processData(data))
.catch((error) => {
// Handles any rejection from fetch, response.json(), or processData
console.error("Request failed:", error.message);
});.catch() is shorthand for .then(undefined, handler). The key feature: it catches rejections from ANY earlier step in the chain, not just the immediately preceding one.
.catch() Returns a Promise
The Promise returned by .catch() is fulfilled (not rejected) if the handler runs without throwing:
Promise.reject(new Error("network error"))
.catch((err) => {
console.error(err.message); // "network error"
return "default value"; // Recovered! Chain continues
})
.then((value) => {
console.log(value); // "default value"
});This enables the recovery pattern — falling back to a default or retrying on failure.
Re-Throwing When You Cannot Recover
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.catch((err) => {
if (err.message.startsWith("HTTP 5")) {
// Server error: log but re-throw (cannot recover here)
logError(err);
throw err;
}
// Other errors: swallow and return fallback
return fallbackData;
})
.then((data) => renderUI(data));Technique 2: try/catch with async/await
When using async/await, wrap await expressions in a try/catch:
async function loadDashboard(userId) {
try {
const user = await fetchUser(userId);
const stats = await fetchStats(user.id);
const posts = await fetchRecentPosts(user.id);
renderDashboard({ user, stats, posts });
} catch (error) {
// Catches rejection from ANY of the three fetch calls above
showErrorMessage(error.message);
}
}Granular try/catch for Different Errors
Sometimes different operations require different error handling:
async function initApp() {
// Config failure: fatal, cannot continue
let config;
try {
config = await loadConfig();
} catch (err) {
console.error("Cannot load config, aborting:", err);
process.exit(1);
}
// Auth failure: redirect to login
let user;
try {
user = await authenticateUser();
} catch (err) {
redirectToLogin();
return;
}
// Data failure: show stale or default data
let data;
try {
data = await fetchDashboardData(user.id);
} catch (err) {
console.warn("Using cached data:", err.message);
data = loadCachedData();
}
renderApp(config, user, data);
}Wrapping await Without try/catch
For cases where you want inline error handling without try/catch blocks, you can use a helper:
// Helper: returns [error, result] tuple
async function attempt(promise) {
try {
const result = await promise;
return [null, result];
} catch (err) {
return [err, null];
}
}
// Usage:
async function getUser(id) {
const [err, user] = await attempt(fetchUser(id));
if (err) {
console.error("Failed to fetch user:", err.message);
return null;
}
return user;
}This pattern (inspired by Go-style error handling) keeps error handling visible without deeply nested try/catch.
Technique 3: The Second Argument to .then()
.then(onFulfilled, onRejected) accepts a rejection handler as its second argument. This is primarily useful when you want to handle a specific step's rejection differently from the rest of the chain:
fetchPrimaryData()
.then(
(data) => processData(data),
(err) => {
// Only handles rejection from fetchPrimaryData
// Does NOT catch errors thrown by processData
return fallbackData;
}
)
.then((result) => saveResult(result))
.catch((err) => handleFinalError(err)); // Catches processData and saveResult errorsThis is a subtle but important difference from .then(a).catch(b) — see the FAQ for full comparison.
Technique 4: The unhandledrejection Event
When a rejected Promise has no handler, the unhandledrejection event fires on window (browsers) or process (Node.js):
// Browser: global unhandled rejection handler
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled rejection:", event.reason);
event.preventDefault(); // Suppress default console warning in some browsers
// In production: send to error tracking
Sentry.captureException(event.reason);
});
// Node.js: process-level handler
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
// In production: log and potentially exit
process.exit(1);
});When Does unhandledrejection Fire?
// This fires unhandledrejection (no .catch())
Promise.reject(new Error("oops"));
// This does NOT fire unhandledrejection (has .catch())
Promise.reject(new Error("oops")).catch(() => {});
// This does NOT fire — .catch() attached eventually (within same microtask tick)
const p = Promise.reject(new Error("oops"));
p.catch(() => {}); // Attaching .catch() synchronously prevents the unhandled eventThe rejectionhandled Event
When a previously unhandled rejection gets a handler later, rejectionhandled fires:
window.addEventListener("rejectionhandled", (event) => {
console.log("Promise rejection was handled after the fact:", event.reason);
});Rejection Chaining Patterns
Failing Fast (Default)
// One bad step stops the chain
Promise.resolve()
.then(() => stepA()) // If this rejects...
.then(() => stepB()) // ...this is skipped
.then(() => stepC()) // ...and this
.catch((err) => handleError(err)); // Only this runsContinue on Partial Failure
// Run all steps but collect errors
async function runAllSteps(items) {
const results = [];
const errors = [];
for (const item of items) {
try {
const result = await processItem(item);
results.push({ item, result, success: true });
} catch (err) {
errors.push({ item, error: err.message, success: false });
}
}
return { results, errors };
}Retry on Rejection
async function fetchWithRetry(url, maxAttempts = 3) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
lastError = err;
if (attempt < maxAttempts) {
const delay = attempt * 1000; // Incremental backoff
console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw new Error(`All ${maxAttempts} attempts failed: ${lastError.message}`);
}Rejection Handling With Promise.all
Promise.all rejects immediately if any Promise rejects (fail-fast). Use Promise.allSettled when you want results from all Promises regardless of outcome:
const ids = [1, 2, 3, 4, 5];
// Promise.all: one failure cancels all
try {
const users = await Promise.all(ids.map((id) => fetchUser(id)));
// All or nothing
} catch (err) {
console.error("At least one user fetch failed:", err);
}
// Promise.allSettled: get results from all, handle errors individually
const results = await Promise.allSettled(ids.map((id) => fetchUser(id)));
const succeeded = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
const failed = results.filter((r) => r.status === "rejected").map((r) => r.reason);
console.log(`${succeeded.length} succeeded, ${failed.length} failed`);For more on Promise.all and related methods, see how to use Promise.all.
Best Practices Table
| Practice | Do This | Not This |
|---|---|---|
| Always handle rejections | .catch() or try/catch on every Promise chain | Let Promises reject silently |
| Rejection reasons | reject(new Error("...")) | reject("string error") |
| Re-throw unknown errors | throw err in .catch() if you can't handle it | Silently swallow all errors |
| Global fallback | Set unhandledrejection handler in app startup | Rely on default browser behavior |
| Granular error types | Custom Error subclasses for categorization | Generic new Error() for everything |
Rune AI
Key Insights
- Never let a Promise chain end without .catch(): A rejected Promise with no handler fails silently; one
.catch()at the end of a chain covers all preceding steps - .catch() recovers by returning a value: Unless the
.catch()handler re-throws, the chain continues in a fulfilled state with the handler's return value - async/await uses try/catch:
awaitcauses a rejected Promise to throw, so standardtry/catchblocks work exactly as with synchronous code - unhandledrejection is a global safety net: Register it at app startup to catch any rejections that slipped through, especially in production where silent failures are invisible
- Reject with Error objects, not strings: Error objects include stack traces; strings do not — always
throw new Error("message")orreject(new Error("message"))for debuggable failures
Frequently Asked Questions
What is the difference between .then(a, b) and .then(a).catch(b)?
Should I throw Error objects or plain strings in rejections?
Does every Promise need its own .catch()?
What happens to a rejected Promise if the garbage collector cleans it up before a handler is attached?
Can I silence unhandledrejection warnings intentionally?
Conclusion
Promise rejections must always be handled — unhandled rejections are silent failures that cause incorrect application behavior. The primary tools are .catch() for chain-based code and try/catch for async/await code. The global unhandledrejection event is a safety net for unexpected rejections that slip through. For parallel operations, Promise.allSettled handles partial failure gracefully while Promise.all uses fail-fast semantics. Understanding Promise chaining provides the foundation for placing error handlers correctly in any chain.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.