Question

How to Reject a Promise in async/await with TypeScript

typescriptjavascriptasynchronouses6-promiseecmascript-2017

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 value becomes Promise.resolve(value)
  • throw error becomes Promise.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 200 means: "This operation succeeded with value 200."
  • throw 400 means: "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.

Take Quiz

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

  1. foo() is called.
  2. Because foo is async, it immediately returns a promise.
  3. Execution pauses at await someAsyncPromise().
  4. If that promise resolves, execution continues.
  5. return 200 runs.
  6. The promise returned by foo() resolves with 200.

If someAsyncPromise() fails

  1. foo() is called.
  2. It immediately returns a promise.
  3. Execution pauses at await someAsyncPromise().
  4. If that promise rejects, control jumps to catch.
  5. throw new Error("Operation failed") runs.
  6. 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.

Take Quiz

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

PatternResultUse when
return value in async functionResolved promiseThe operation succeeded
throw error in async functionRejected promiseThe operation failed
return Promise.resolve(value)Resolved promiseUsually outside async functions
return Promise.reject(error)Rejected promiseUsually outside async functions
catch { return fallback; }Resolved promise with fallbackYou want recovery
catch { throw error; }Rejected promiseYou want failure to propagate

async/await vs .then().catch()

StyleSuccessFailure
Promise chain.then(value => ...).catch(error => ...)
async/awaitreturn valuethrow error

return vs throw inside catch

CodeOutcome
catch { return 400; }Promise resolves with 400
catch { throw 400; }Promise rejects with 400
catch { throw new Error("Failed"); }Promise rejects with an Error
Take Quiz

Cheat Sheet

async function example() {
  return 123; // resolves with 123
}

async function example2() {
  throw new Error("Failed"); // rejects
}

Rules

  • async functions always return a promise
  • return value => resolved promise
  • throw error => rejected promise
  • await pauses until a promise settles
  • rejected await jumps to catch

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(...) over throw 400
  • Only return fallback values in catch when recovery is intentional
  • Do not wrap code in try/catch unless 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.

Take Quiz

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 Error when the request fails.
  • Show how to call the function with both await and .then().catch().
Take Quiz

Keep learning

Related questions

View more