Common Async TypeScript Mistakes

Avoid the most common async TypeScript mistakes. Covers floating promises, missing error handling, Promise.all pitfalls, and async function misunderstandings.

7 min read

Async TypeScript mistakes are patterns that compile without errors but produce bugs at runtime. They happen because TypeScript trusts you to handle promises correctly, and the compiler does not warn about every async pitfall. Knowing these patterns helps you spot them in code review and avoid them while writing.

Mistake 1: Forgetting await

The most common async mistake is calling an async function without await. The result is a promise where you expected a value.

typescripttypescript
async function getCount(): Promise<number> {
  return 5;
}
 
const result = getCount();
console.log(result + 1);

This compiles but logs something unexpected. The variable result is a Promise of number, not a number. Adding 1 to a promise produces the string " [object Promise]1", which is almost certainly not what you intended.

The fix is to await the call. If you cannot use await in the current context, use .then() to access the resolved value. TypeScript catches this mistake when you annotate the variable type, because Promise of number is not assignable to number.

Mistake 2: Not Handling the Error Type

The error in a catch block is unknown in strict mode. Accessing properties without narrowing produces a compile error, but many developers work around it by disabling strict mode or using type assertions.

typescripttypescript
try {
  await riskyOperation();
} catch (error) {
  console.log(error.message);
}

This produces a compiler error because unknown has no message property. The fix is to narrow the error type before accessing any properties on it.

typescripttypescript
try {
  await riskyOperation();
} catch (error) {
  if (error instanceof Error) {
    console.log(error.message);
  } else {
    console.log("Unknown error:", error);
  }
}

The else branch handles the case where something other than an Error was thrown. This might seem unlikely, but JavaScript allows throwing strings, numbers, and even undefined. The unknown type forces you to be prepared for anything.

Mistake 3: Floating Promises

A floating promise is a promise that is created but never handled. If it rejects, the error disappears silently.

typescripttypescript
async function sendAnalytics(event: string) {
  await fetch("/api/analytics", {
    method: "POST",
    body: JSON.stringify({ event }),
  });
}
 
function handleClick() {
  sendAnalytics("button-click");
}

The handleClick function calls sendAnalytics without await. The analytics request still fires, but if it fails, the error is lost. The function also does not wait for the request to complete before the caller moves on.

The fix depends on intent. If you need the result, await the call. If the call is fire-and-forget and errors are acceptable, add a .catch() to explicitly handle or log rejections.

Mistake 4: Using Promise.all with Dynamic Arrays

When you build an array of promises dynamically and pass it to Promise.all, TypeScript cannot infer the tuple type. The result widens to an array of the union type.

typescripttypescript
const promises: Promise<unknown>[] = [];
 
if (shouldFetchUser) {
  promises.push(fetchUser(1));
}
if (shouldFetchPosts) {
  promises.push(fetchPosts(1));
}
 
const results = await Promise.all(promises);

The results are typed as unknown[], losing all type information. Each element requires a type assertion to use. The fix is to keep the promises in a typed tuple and pass it directly to Promise.all, or to use Promise.allSettled if the array is genuinely dynamic and you need to handle heterogeneous results.

Mistake 5: Returning from .catch() Without Understanding the Type Change

A return statement inside a .catch() callback changes the resolved type of the promise chain. This is a feature, not a bug, but it surprises developers who do not expect it.

typescripttypescript
const result = await fetchUser(1)
  .catch(() => null);

The type of result is User or null, because the .catch() callback returns null. If the fetch succeeds, result is a User. If it fails, result is null. This pattern is useful for providing fallback values, but the type change can cause downstream code to fail narrowing checks.

The fix is to narrow the result before using it, just as you would with any union type. Check for null before accessing user properties.

Mistake 6: Mixing await and .then() in the Same Function

Both await and .then() work with TypeScript's type system, but mixing them in the same function makes the type flow hard to follow.

typescripttypescript
async function loadData() {
  const user = await fetchUser(1);
  const posts = fetchPosts(user.id).then((p) => p.slice(0, 5));
  return { user, posts };
}

The posts variable is a Promise, not an array. The function returns an object where posts is a pending promise, which is probably not intended. The fix is to pick one pattern: either await every async operation or chain everything with .then(). Mixing them creates confusion about which variables are values and which are promises.

Mistake 7: Forgetting That Async Functions Always Return Promises

An async function wraps its return value in a promise, even if you never use await inside it.

typescripttypescript
async function double(n: number) {
  return n * 2;
}
 
const result = double(5);

The variable result is a Promise of number, not number. Even though the function body is entirely synchronous, the async keyword forces the return type to be a promise. If you need the function to return a plain value, remove the async keyword. If you need it to be async for other reasons, callers must await the result.

For more on how these patterns interact, see async and await in TypeScript and promises in TypeScript explained. For structured error handling, see handle API errors in TypeScript.

Rune AI

Rune AI

Key Insights

  • Always await promises. A floating promise silently loses rejections.
  • Errors in catch are unknown. Always narrow with instanceof before accessing properties.
  • Returning from .catch() changes the resolved type. Use this for fallback values.
  • Do not mix await and .then() in the same function. Pick one pattern.
  • Async functions always return promises. Forgetting this causes type mismatches.
RunePowered by Rune AI

Frequently Asked Questions

What is a floating promise in TypeScript?

A floating promise is a promise that is created but never awaited or handled with .catch(). If the promise rejects, the error is silently lost. TypeScript can catch this with the no-floating-promises lint rule, but the compiler itself does not warn about it.

Why does forgetting await not always cause a compiler error?

If the function you forgot to await is typed as returning Promise<T> and you assign the result to a variable typed as T, TypeScript catches the mismatch. But if you pass the promise to a function that accepts any, or log it directly, the compiler might not warn you.

Is returning a value from a .catch() handler a good pattern?

Yes, it is called error recovery. Returning a fallback value from .catch() changes the promise chain's resolved type to include the fallback. This lets you provide defaults when specific operations fail without crashing the entire chain.

Conclusion

Async TypeScript mistakes fall into predictable categories: forgetting await, mishandling errors, misusing Promise.all, and misunderstanding how async functions wrap return types. Each mistake has a clear fix that the type system can help enforce. The key habit is to always handle the error path, always await your promises, and always let the types guide you to the correct async pattern.