Tutorials Logic, IN info@tutorialslogic.com

Unhandled Promise Rejection in JavaScript: Causes and Fixes

Promise Rejection Ownership

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.

What is Unhandled Promise Rejection?

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.

Failure Causes

  • Missing .catch() handler on promises
  • No try-catch block in async/await functions
  • API request fails without error handling
  • Throwing errors inside promises without catching
  • Chaining promises without final .catch()

Immediate Repair

Immediate Fix: [wrong] Problem - No error handling

Immediate Fix: [wrong] Problem - No error handling
// [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);
    }
}

Repair Scenarios

  • The most common case - making API calls without handling potential errors.
  • Using async/await without try-catch blocks leads to unhandled rejections.
  • Long promise chains need a final .catch() to handle any errors in the chain.
  • When using Promise.all(), if any promise rejects, the entire operation fails.

Failure: No error handling - will cause unhandled rejection if

Failure: No error handling - will cause unhandled rejection if
// 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

Correction: Add .catch() at the end

Correction: Add .catch() at the end
// 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);
    });

Failure: If fetch fails, unhandled rejection

Failure: If fetch fails, unhandled rejection
async function loadUserData() {
    const response = await fetch('/api/user');
    const user = await response.json();
    console.log(user);
}

loadUserData(); // If fetch fails, unhandled rejection!

Correction: Add try-catch inside function

Correction: Add try-catch inside function
// 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);
});

Failure: Any error in this chain is unhandled

Failure: Any error in this chain is unhandled
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

Correction: Add .catch() at the end of the chain

Correction: Add .catch() at the end of the chain
// 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();

Failure: If any fetch fails, unhandled rejection

Failure: If any fetch fails, unhandled rejection
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

Correction: Add .catch() to Promise.all()

Correction: Add .catch() to Promise.all()
// 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);
});

Global Error Handlers

Global Handler

Global Handler
// 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
});

Node.js Handler

Node.js Handler
process.on('unhandledRejection', (reason, promise) => {
    console.error('Unhandled Rejection at:', promise, 'reason:', reason);
    // Log to error tracking service
    // Optionally exit process
    // process.exit(1);
});

Prevention Practices

  • Always add .catch() - Every promise chain should end with .catch()
  • Use try-catch with async/await - Wrap await calls in try-catch blocks
  • Handle errors at appropriate level - Catch errors where you can handle them meaningfully
  • Use Promise.allSettled() - When you want all promises to complete regardless of failures
  • Add global handlers - Catch any unhandled rejections as a safety net
  • Log errors properly - Use error tracking services like Sentry
  • Show user-friendly messages - Don't expose technical errors to users

Rejection Propagation and Ownership

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.

  • Return, await, or deliberately own every Promise.
  • Remember that every chain method creates a new Promise.
  • Recover only with a valid substitute result.
  • Reject with structured Error objects and stable categories.

Async Error Boundaries

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.

  • Await inside the try block that owns recovery.
  • Catch at decision boundaries instead of every layer.
  • Keep finally cleanup from replacing the original outcome.
  • Pair composition with underlying operation cancellation.

Unhandled Rejection Reporting

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.

  • Use global rejection events for telemetry, not recovery.
  • Account for late handlers and host-specific policy.
  • Suppress default reporting only after equivalent capture.
  • Group privacy-safe reports by release and operation context.

Cancellation and Retry

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.

  • Cancel the underlying resource, not only the waiting Promise.
  • Classify expected aborts separately from failures.
  • Retry only eligible operations under bounded policy.
  • Correlate attempts with one overall operation.

Rejection Tests and Diagnostics

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.

  • Await every asynchronous test assertion.
  • Control settlement order without arbitrary sleeps.
  • Inspect async stacks with matching source maps.
  • Assert state, side effects, reporting, and cleanup.

Rejection Handling Examples

Handle fetch and HTTP failures

Handle fetch and HTTP failures
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);
});

Keep partial results with Promise.allSettled

Keep partial results with Promise.allSettled
const results = await Promise.allSettled([
  fetch("/api/profile"),
  fetch("/api/notifications"),
]);

for (const result of results) {
  if (result.status === "rejected") {
    console.error(result.reason);
  }
}
Before you move on

Unhandled Promise Rejection in JavaScript: Causes and Fixes Mastery Check

5 checks
  • Return, await, or explicitly own every Promise.
  • Catch only where a recovery or reporting decision exists.
  • Cancel underlying work and classify expected aborts.
  • Retry only eligible operations under a bounded policy.
  • Test state, side effects, telemetry, and cleanup.

JavaScript Questions Learners Ask

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.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.