Common Async TypeScript Mistakes
Avoid the most common async TypeScript mistakes. Covers floating promises, missing error handling, Promise.all pitfalls, and async function misunderstandings.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
Frequently Asked Questions
What is a floating promise in TypeScript?
Why does forgetting await not always cause a compiler error?
Is returning a value from a .catch() handler a good pattern?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.