How to Properly Reject in async/await? A TypeScript Breakdown

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Async/Await and reject: why is it a pain for beginners?

\n

Have you ever written an async function, waited for a response from the server, and then caught an UnhandledPromiseRejection? Sound familiar? This is one of the most common problems developers face when transitioning to async/await in TypeScript.

\n\n

On Stack Overflow, the question "How to reject in async/await syntax?" has garnered thousands of views. And for good reason. At first glance, it seems enough to simply return Promise.reject() — and you're done. But in real projects, things are more complex: types, call chains, loss of error context.

\n\n

In this guide, we'll break down how to properly reject promises in TypeScript async functions, learn to distinguish throw from return Promise.reject(), and look at practical examples from real-world projects.

\n\n

Basics: how reject works in async/await

\n

Before diving into the details, let's recall the fundamentals. Any async function in TypeScript returns a Promise. If a throw occurs inside it, or a Promise.reject() is returned, the promise transitions to the rejected state.

\n\n
// Simplest example of reject in async/await\nasync function fetchUser(id: number): Promise<User> {\n    if (id <= 0) {\n        throw new Error('Invalid user ID');\n    }\n    // ...logic to fetch the user\n}
\n\n

Here, throw automatically turns into a reject. But what if we want to return an already rejected promise?

\n\n

throw vs return Promise.reject()

\n

In TypeScript, both approaches work, but there are nuances:

\n\n
    \n
  • throw — a more idiomatic way. It stops the function execution and passes control to the nearest catch.
  • \n
  • return Promise.reject() — explicitly returns a rejected promise. Useful when you need to pass an error from another function.
  • \n
\n\n
// Difference between throw and return Promise.reject()\nasync function processOrder(orderId: string): Promise<OrderStatus> {\n    // Option 1: throw\n    if (!orderId) {\n        throw new Error('Order ID is required');\n    }\n    \n    // Option 2: return Promise.reject\n    if (orderId.startsWith('test_')) {\n        return Promise.reject(new Error('Test orders are not allowed'));\n    }\n    \n    return { status: 'processing' };\n}
\n\n

Important: TypeScript does not always correctly infer types with return Promise.reject(). If a function expects Promise<T> and you return Promise.reject(), a typing error may occur. In such cases, it's better to use throw.

\n\n

Common mistakes when rejecting in async functions

\n

Even experienced TypeScript developers sometimes make classic mistakes. Here are the top 3:

\n\n

1. Forgot to handle reject in the calling code

\n
async function main() {\n    // Bad: no try/catch\n    const user = await fetchUser(-1);\n    console.log(user); // UnhandledPromiseRejection!\n}\n\n// Good:\nasync function mainSafe() {\n    try {\n        const user = await fetchUser(-1);\n        console.log(user);\n    } catch (error) {\n        console.error('Failed to fetch user:', error);\n    }\n}
\n\n

2. Incorrect error typing

\n

In TypeScript, the catch block by default catches unknown. This is correct, but sometimes developers try to access error.message without checking.

\n\n
try {\n    await riskyOperation();\n} catch (error) {\n    // Error: error — unknown\n    console.log(error.message); // ❌ Property 'message' does not exist on type 'unknown'\n    \n    // Correct:\n    if (error instanceof Error) {\n        console.log(error.message);\n    } else {\n        console.log                

Blogs

Book Recommendations