A Promise rejection must travel to an owner that can recover, cancel, report, or propagate it. Chain methods create new Promises, catches can accidentally convert failure to success, and global unhandled-rejection events are telemetry boundaries rather than business recovery.
Reliable async code uses structured errors, bounded retry, underlying-operation cancellation, deterministic settlement tests, and one reporting boundary with preserved cause and operation context.
Unhandled Promise Rejection occurs when a Promise is rejected but there's no .catch() handler or try-catch block to handle the error. This can lead to silent failures and hard-to-debug issues in your application.
// [wrong] Problem - No error handling
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data));
// [ok] Solution 1: Add .catch()
fetch('/api/users')
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
// [ok] Solution 2: Use async/await with try-catch
async function getUsers() {
try {
const res = await fetch('/api/users');
const data = await res.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
// No error handling - will cause unhandled rejection if API fails
fetch('https://api.example.com/users')
.then(response => response.json())
.then(users => {
console.log(users);
displayUsers(users);
});
// If network fails or API returns error, promise is rejected but not caught
// Solution 1: Add .catch() at the end
fetch('https://api.example.com/users')
.then(response => response.json())
.then(users => {
console.log(users);
displayUsers(users);
})
.catch(error => {
console.error('Failed to fetch users:', error);
showErrorMessage('Unable to load users');
});
// Solution 2: Handle HTTP errors properly
fetch('https://api.example.com/users')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(users => {
console.log(users);
displayUsers(users);
})
.catch(error => {
console.error('Error:', error);
showErrorMessage(error.message);
});
async function loadUserData() {
const response = await fetch('/api/user');
const user = await response.json();
console.log(user);
}
loadUserData(); // If fetch fails, unhandled rejection!
// Solution 1: Add try-catch inside function
async function loadUserData() {
try {
const response = await fetch('/api/user');
const user = await response.json();
console.log(user);
} catch (error) {
console.error('Failed to load user:', error);
}
}
loadUserData();
// Solution 2: Catch when calling the function
async function loadUserData() {
const response = await fetch('/api/user');
const user = await response.json();
return user;
}
loadUserData()
.then(user => console.log(user))
.catch(error => console.error('Error:', error));
// Solution 3: Use .catch() on the promise
loadUserData().catch(error => {
console.error('Error:', error);
});
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(res => res.json())
.then(posts => {
console.log(posts);
displayPosts(posts);
});
// Any error in this chain is unhandled
// Add .catch() at the end of the chain
fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/posts/${user.id}`))
.then(res => res.json())
.then(posts => {
console.log(posts);
displayPosts(posts);
})
.catch(error => {
console.error('Error in promise chain:', error);
showErrorMessage('Failed to load data');
});
// Or use async/await for cleaner code
async function loadUserPosts() {
try {
const userRes = await fetch('/api/user');
const user = await userRes.json();
const postsRes = await fetch(`/api/posts/${user.id}`);
const posts = await postsRes.json();
displayPosts(posts);
} catch (error) {
console.error('Error:', error);
showErrorMessage('Failed to load data');
}
}
loadUserPosts();
Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([users, posts, comments]) => {
console.log(users, posts, comments);
});
// If any fetch fails, unhandled rejection
// Solution 1: Add .catch() to Promise.all()
Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([users, posts, comments]) => {
console.log(users, posts, comments);
})
.catch(error => {
console.error('Failed to load data:', error);
});
// Solution 2: Use Promise.allSettled() (doesn't reject)
Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Request ${index} succeeded:`, result.value);
} else {
console.error(`Request ${index} failed:`, result.reason);
}
});
});
// Solution 3: Catch individual promises
Promise.all([
fetch('/api/users').catch(err => ({ error: err })),
fetch('/api/posts').catch(err => ({ error: err })),
fetch('/api/comments').catch(err => ({ error: err }))
])
.then(results => {
// Handle results, some may have errors
console.log(results);
});
// Catch all unhandled promise rejections
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled promise rejection:', event.reason);
// Log to error tracking service
// Show user-friendly error message
event.preventDefault(); // Prevent default browser behavior
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
// Log to error tracking service
// Optionally exit process
// process.exit(1);
});
A Promise rejection represents an asynchronous operation that did not produce its normal fulfillment value. A rejection travels through fulfillment-only handlers until a rejection handler is attached. Each `then`, `catch`, and `finally` call returns a new Promise, so the unhandled Promise may be a later chain result rather than the original operation.
Every created Promise needs an owner that returns it, awaits it, or attaches a meaningful rejection handler. Calling an async function without observing its result creates fire-and-forget work. If that work is intentional, give it explicit cancellation, logging, and lifecycle ownership instead of suppressing it with an empty catch.
A catch handler that returns normally converts the chain to fulfillment. Use that only when it has produced a valid fallback result. To add context without claiming recovery, throw a new typed error with the original error as `cause`, or rethrow the original when no additional information is needed.
Throw Error objects rather than strings or arbitrary payloads. Stable error classes and codes allow a boundary to distinguish validation, cancellation, authorization, conflict, transient dependency failure, and programming defects without parsing human-facing text.
A synchronous try/catch catches an awaited rejection only when the `await` occurs inside the try block. It does not catch a Promise merely returned or started there. Either await the operation at that boundary or return the Promise to a caller that owns the error.
Place catches where a decision can be made: retry, show a user state, translate a protocol error, roll back work, or report final failure. Catching and logging at every layer duplicates telemetry and often strips context. Lower layers should add structured context while the owning boundary reports once.
`finally` is for cleanup that must occur after either outcome. It does not normally receive the result. If finally throws or returns a rejecting Promise, that new failure replaces the prior outcome, so cleanup should be narrow, dependable, and separately tested.
Promise combinators have different failure contracts. `all` reports the first observed rejection without canceling remaining operations, `allSettled` collects every outcome, `any` succeeds on the first fulfillment, and `race` follows the first settlement. Choose from product semantics and cancel losing work through the underlying APIs.
Browsers dispatch `unhandledrejection` to a global scope when a rejected Promise lacks a handler at the reporting point. The event exposes the Promise and rejection reason. It is a last-resort diagnostic signal, not a replacement for local ownership, because the global handler does not know the correct business recovery.
A handler attached later can lead to a subsequent handled-rejection signal, and cross-origin privacy rules may limit browser reporting. Runtime behavior and process policy differ across browsers, workers, and server JavaScript environments. Test supported hosts rather than assuming one console warning or termination policy.
Calling `preventDefault()` can suppress the runtime default report in supporting browser events. Do this only when equivalent telemetry is reliably captured; otherwise it hides evidence. Never show raw rejection reasons to users because they may contain implementation details or sensitive data.
Group reports by normalized error type, code, top application frame, route or operation, and release. Preserve cause chains and request IDs, redact secrets, and sample high-volume failures. A global count without operation ownership cannot tell whether data was partially committed.
Promise itself has no universal cancel method. Pass an `AbortSignal` to fetch and other compatible APIs, and design custom producers to stop timers, subscriptions, streams, or workers when aborted. Racing a timeout changes which Promise settles first but does not stop the losing operation.
Treat cancellation as an expected control outcome when the user navigates, replaces a search, or closes a view. Do not report every abort as an application failure. Still verify that cleanup runs and that partial side effects are either prevented, committed, or reconciled according to the operation contract.
Retry only transient, idempotent, or explicitly deduplicated work. Bound attempts, add jittered backoff, honor server guidance, retain a total deadline, and stop on cancellation. Retrying authentication, validation, permission, or deterministic programming errors wastes capacity and hides the cause.
A retry attempt needs its own identifier and cause while remaining linked to the overall operation. Record attempt count and final disposition. Do not log every expected failed attempt at error level and then log the final failure again without correlation.
Test fulfillment, each documented rejection category, cancellation, timeout, cleanup failure, and invalid recovery. Await the assertion so the test runner owns the Promise. A test that starts an async assertion without returning or awaiting it can pass while the rejection appears later as unrelated noise.
Use controllable deferred dependencies to settle operations in specific orders. Verify stale results are ignored, losing work is canceled, and concurrent failures do not create duplicate user messages. Avoid arbitrary sleeps; they are slow and do not establish ordering deterministically.
Attach temporary breakpoints on caught and uncaught Promise rejections, then inspect the creation and handling chain with async stack support. Source maps must match the deployed chunk. Preserve error cause and operation metadata when crossing worker or message boundaries.
The regression should assert the final state, side effects, telemetry count, and resource cleanup, not merely that a catch ran. A handled rejection can still leave corrupted state, and an unhandled-rejection-free run can still abandon required work silently.
async function loadUser(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Unable to load user", error);
return null;
}
}
loadUser(42).then(user => {
if (user) renderUser(user);
});
const results = await Promise.allSettled([
fetch("/api/profile"),
fetch("/api/notifications"),
]);
for (const result of results) {
if (result.status === "rejected") {
console.error(result.reason);
}
}
Unhandled promise rejection occurs when a Promise is rejected but there's no .catch() handler or try-catch block to handle the error. This can cause silent failures and make debugging difficult.
Add .catch() handler to promise chains or use try-catch blocks with async/await. Every promise should have error handling either inline or at the end of the chain.
In browsers, it logs a warning to console. In Node.js, it logs a warning and may crash the process in future versions. Always handle rejections to prevent unexpected behavior.
Explore 500+ free tutorials across 20+ languages and frameworks.