Question
How to Reject a Promise in async/await with TypeScript
Question
How can you reject a promise returned by an async function?
For example, suppose you originally had code like this:
function foo(id: string): Promise<number> {
return new Promise((resolve, reject) => {
someAsyncPromise()
.then(() => resolve(200))
.catch(() => reject(400));
});
}
Now you want to rewrite it using async/await:
async function foo(id: string): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch (error) {
return 400;
}
}
The issue is that return 400 inside catch produces a resolved promise with value 400, not a rejected promise.
How do you correctly make an async function return a rejected promise in this case?
Short Answer
By the end of this page, you will understand how async functions wrap return values in resolved promises, how errors become rejected promises, and why throw is the correct way to reject from async/await code. You will also see practical TypeScript examples, common mistakes, and patterns used in real projects.
Concept
An async function always returns a promise.
That leads to two important rules:
return valuebecomesPromise.resolve(value)throw errorbecomesPromise.reject(error)
This is the key idea behind rejecting in async/await syntax.
In promise chains, you often write:
return Promise.reject(error);
But in an async function, the more natural equivalent is:
throw error;
So if you want your async function to fail, you should throw, not return a normal value.
Why this matters
If you accidentally return a fallback value inside catch, callers will see the operation as successful, even though something actually failed. That can hide bugs and make error handling confusing.
For example:
async function foo(): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch {
return 400;
}
}
This does not reject. It resolves with 400.
If your intention is failure, do this instead:
async function foo(): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch {
throw 400;
}
}
Now the returned promise is rejected.
Important note
Although throw 400 works technically, in real code developers usually throw an Error object rather than a raw number or string:
throw new Error("Request failed");
This gives better stack traces and clearer debugging information.
Mental Model
Think of an async function as a machine that automatically wraps your output.
- If you hand it a value with
return, it puts that value in a success box. - If you throw an error, it puts that error in a failure box.
So:
return 200means: "This operation succeeded with value 200."throw 400means: "This operation failed with reason 400."
A catch block does not automatically reject. It only gives you a chance to decide what happens next.
Inside catch, you can:
- recover and return a default value
- transform the error and throw a new one
- rethrow the same error
The important part is that return = success, throw = failure.
Syntax and Examples
The core syntax is simple.
Return a resolved promise from an async function
async function successExample(): Promise<number> {
return 200;
}
This is equivalent to:
function successExample(): Promise<number> {
return Promise.resolve(200);
}
Return a rejected promise from an async function
async function failureExample(): Promise<number> {
throw new Error("Something went wrong");
}
This is equivalent to:
function failureExample(): Promise<number> {
return Promise.reject(new Error("Something went wrong"));
}
Your example rewritten correctly
async function foo(id: string): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch (error) {
throw 400;
}
}
Better TypeScript version
async function foo(id: string): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch (error) {
throw new Error("Copy failed");
}
}
Example of transforming an error
async function loadUser(): Promise<string> {
try {
return await fetchUserName();
} catch (error) {
throw new Error("Unable to load user");
}
}
Here, the function catches one error and throws a clearer one for the caller.
Step by Step Execution
Consider this example:
async function foo(): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch (error) {
throw new Error("Operation failed");
}
}
If someAsyncPromise() succeeds
foo()is called.- Because
fooisasync, it immediately returns a promise. - Execution pauses at
await someAsyncPromise(). - If that promise resolves, execution continues.
return 200runs.- The promise returned by
foo()resolves with200.
If someAsyncPromise() fails
foo()is called.- It immediately returns a promise.
- Execution pauses at
await someAsyncPromise(). - If that promise rejects, control jumps to
catch. throw new Error("Operation failed")runs.- The promise returned by
foo()rejects with that error.
Small trace example
async function demo(shouldFail: boolean): Promise<string> {
try {
if (shouldFail) {
throw new Error("Inner failure");
}
return "success";
} catch (error) {
throw new Error("Outer failure");
}
}
demo(false)
- no error is thrown
- returns
"success" - promise resolves
demo(true)
"Inner failure"is thrown- control moves to
catch "Outer failure"is thrown- promise rejects
Real World Use Cases
Rejecting correctly in async functions is important in many common situations.
API requests
If a request fails, the function should reject so the caller can show an error message:
async function getProfile(): Promise<User> {
const response = await fetch("/api/profile");
if (!response.ok) {
throw new Error("Failed to fetch profile");
}
return response.json();
}
Validation
Reject when input is invalid:
async function saveEmail(email: string): Promise<void> {
if (!email.includes("@")) {
throw new Error("Invalid email address");
}
await saveToDatabase(email);
}
File or database operations
If a resource is missing or inaccessible, reject instead of returning a fake success value.
async function readConfig(): Promise<string> {
const config = await loadConfigFile();
if (!config) {
throw new Error("Config file not found");
}
return config;
}
Background jobs
A failed job should reject so monitoring and retries can happen properly.
Real Codebase Usage
In real projects, developers usually do more than just throw inside catch. They use patterns that make error handling clearer and more maintainable.
1. Rethrow the original error
If you just want the failure to continue upward:
async function processOrder(): Promise<void> {
try {
await chargeCustomer();
} catch (error) {
throw error;
}
}
This works, though sometimes the try/catch is unnecessary if you are not adding anything.
2. Throw a more meaningful error
async function processOrder(): Promise<void> {
try {
await chargeCustomer();
} catch (error) {
throw new Error("Payment processing failed");
}
}
This is useful when low-level errors need to be translated into business-level messages.
3. Guard clauses before async work
async function createUser(name: string): Promise<void> {
if (!name.trim()) {
throw new Error("Name is required");
}
await saveUser(name);
}
4. Early return for valid fallback behavior
Sometimes returning a value in catch is correct if failure is acceptable:
async function getTheme(): Promise<string> {
try {
return await fetchTheme();
} catch {
return "light";
}
}
This is not rejection. It is intentional recovery.
5. Error wrapping
In larger apps, you may preserve context:
async function loadDashboard(): Promise<Data> {
try {
return await fetchDashboardData();
} catch (error) {
throw new Error("Dashboard data could not be loaded");
}
}
The main idea in real code is to choose clearly between:
- recovering with
return - failing with
throw
Common Mistakes
1. Returning an error value instead of throwing
Broken example:
async function foo(): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch {
return 400;
}
}
Problem:
- This resolves with
400 - It does not reject
Fix:
async function foo(): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch {
throw new Error("Operation failed");
}
}
2. Catching an error and silently hiding it
Broken example:
async function loadData(): Promise<string | undefined> {
try {
return await fetchData();
} catch {
return undefined;
}
}
Problem:
- Callers may not realize something failed
- Debugging becomes harder
Avoid this unless fallback behavior is intentional.
3. Throwing plain numbers or strings
Works technically:
throw 400;
Better:
throw new Error("Request failed");
Why?
- clearer meaning
- stack trace support
- easier debugging
4. Using try/catch when you are not handling anything
Unnecessary code:
async function load(): Promise<Data> {
try {
return await fetchData();
} catch (error) {
throw error;
}
}
Simpler:
async function load(): Promise<Data> {
return fetchData();
}
5. Forgetting that async always returns a promise
Even this:
async function x() {
return 5;
}
actually returns Promise<number>, not number. That is why return and throw affect promise resolution and rejection.
Comparisons
| Pattern | Result | Use when |
|---|---|---|
return value in async function | Resolved promise | The operation succeeded |
throw error in async function | Rejected promise | The operation failed |
return Promise.resolve(value) | Resolved promise | Usually outside async functions |
return Promise.reject(error) | Rejected promise | Usually outside async functions |
catch { return fallback; } | Resolved promise with fallback | You want recovery |
catch { throw error; } | Rejected promise | You want failure to propagate |
async/await vs .then().catch()
| Style | Success | Failure |
|---|---|---|
| Promise chain | .then(value => ...) | .catch(error => ...) |
| async/await | return value | throw error |
return vs throw inside catch
| Code | Outcome |
|---|---|
catch { return 400; } | Promise resolves with 400 |
catch { throw 400; } | Promise rejects with 400 |
catch { throw new Error("Failed"); } | Promise rejects with an Error |
Cheat Sheet
async function example() {
return 123; // resolves with 123
}
async function example2() {
throw new Error("Failed"); // rejects
}
Rules
asyncfunctions always return a promisereturn value=> resolved promisethrow error=> rejected promiseawaitpauses until a promise settles- rejected
awaitjumps tocatch
Correct rejection pattern
async function foo(): Promise<number> {
try {
await someAsyncPromise();
return 200;
} catch (error) {
throw new Error("Operation failed");
}
}
Equivalent non-async patterns
return Promise.resolve(200);
return Promise.reject(new Error("Operation failed"));
Good practice
- Prefer
throw new Error(...)overthrow 400 - Only return fallback values in
catchwhen recovery is intentional - Do not wrap code in
try/catchunless you need to handle or transform the error
Edge case
This resolves, not rejects:
catch {
return 400;
}
FAQ
How do you reject a promise inside an async function?
Use throw, not return. In an async function, throw turns into a rejected promise.
Is return Promise.reject(...) allowed inside an async function?
Yes, but throw is clearer and more idiomatic in async/await code.
Why does return 400 not reject?
Because async functions treat returned values as successful results. It becomes a resolved promise with value 400.
Should I throw numbers like 400?
You can, but it is better to throw an Error object for better debugging and clearer code.
Do I always need a try/catch with await?
No. If you are not recovering from or transforming the error, you can let it bubble up naturally.
What happens if an awaited promise rejects and there is no catch block?
The async function returns a rejected promise automatically.
When should I return a value in catch?
Return a value only when you want to recover with a fallback result instead of treating the situation as a failure.
Mini Project
Description
Build a small TypeScript function that simulates loading user data from a server. The project demonstrates the difference between successful resolution, intentional fallback values, and proper rejection using throw inside an async function.
Goal
Create an async function that returns user data on success and rejects with an error on failure.
Requirements
- Create an async function named
getUserName. - Simulate an async request that may succeed or fail.
- Return a username when the request succeeds.
- Reject with an
Errorwhen the request fails. - Show how to call the function with both
awaitand.then().catch().
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.