Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
TypeScriptJavaScriptJune 20, 20264 min read

Typing async code in TypeScript without fighting the compiler

Typing async code in TypeScript doesn't have to be a battle. Learn how to handle promises, API responses, and error states without fighting the compiler.

TypeScriptJavaScriptAsyncPromisesWeb DevelopmentFrontend
Detailed view of programming code in a dark theme on a computer screen.

We’ve all been there: staring at a red squiggly line in VS Code because the compiler can’t figure out what your fetch call is returning. You’re just trying to map over some data, but TypeScript is convinced your response is unknown, and suddenly your morning is gone.

I remember refactoring a legacy dashboard last year where I spent about four hours just wrestling with Promise<any> definitions. It was a mess. Once I learned how to properly define return types and leverage the compiler instead of fighting it, the code didn't just get safer—it got significantly easier to read.

Why typing async code feels like a fight

The primary reason we struggle is that JavaScript’s async/await syntax makes everything look synchronous, but the underlying types are deeply nested. When you write const data = await fetchData(), the compiler has to track the Promise wrapper, the generic type inside it, and the potential for a rejected state.

We often reach for any as a quick escape hatch. Don’t do it. When you use any, you lose the benefits of TypeScript narrowing: How to make the compiler trust your code, which is essentially the superpower that makes TS worth using in the first place.

The "wrong" way to start

Early on, I used to do this:

TYPESCRIPT
async function getUser(id: string): Promise<any> {
  const response = await fetch(CE9178">`/api/users/${id}`);
  return response.json();
}

The problem here is that getUser promises something, but the rest of my application has no idea what that something is. I’m essentially lying to the compiler. When I try to access user.email, I’m flying blind.

Defining clear interfaces

Instead of hiding the shape of your data, bring it into the light. If you’re working with an API, define an interface that matches the expected response.

TYPESCRIPT
interface User {
  id: string;
  email: string;
  role: CE9178">'admin' | CE9178">'user';
}

async function getUser(id: string): Promise<User> {
  const response = await fetch(CE9178">`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(CE9178">'Failed to fetch user');
  }
  return response.json() as Promise<User>;
}

By explicitly typing the return, you’ve given the compiler a map. Now, when you call getUser, the compiler knows exactly what properties are available. If you try to access user.username, you’ll get a helpful error message instead of a runtime undefined.

Handling errors in async flows

Vibrant JavaScript code displayed on a screen, highlighting programming concepts and software development.

One thing that still catches me off guard is how to type errors. In TypeScript 4.4 and later, catch blocks default to unknown. This forces you to handle the error type safely.

TYPESCRIPT
try {
  const user = await getUser(CE9178">'123');
} catch (err) {
  if (err instanceof Error) {
    console.error(err.message);
  } else {
    console.error(CE9178">'An unexpected error occurred');
  }
}

This pattern is non-negotiable. If you try to access err.message without that instanceof check, the compiler will stop you. It’s annoying in the moment, but it’s saved me from countless production crashes where a non-Error object was thrown.

Advanced patterns: Discriminated Unions

Sometimes, your async function might return different states—like a success object or an error state. Instead of just throwing, you can return a result object. This is where Discriminated unions in TypeScript: Modeling state without bugs really shines.

TYPESCRIPT
type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

async function safeFetch<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    const data = await res.json();
    return { success: true, data };
  } catch (e) {
    return { success: false, error: CE9178">'Request failed' };
  }
}

Now, the consumer of safeFetch is forced to check the success property. The compiler will literally refuse to let you touch the data unless you’ve verified that success is true.

FAQ

Q: Should I use async/await or .then()? A: Use async/await whenever possible. It keeps your code flat and makes error handling via try/catch much more intuitive.

Q: What if the API response is unpredictable? A: Use a validation library like Zod. You can parse the data at the edge of your application to ensure it matches your interface before it propagates through your code.

Q: Does typing everything slow down development? A: In the short term, yes. You’ll spend an extra 2-3 minutes defining types. In the long term, it saves you hours of debugging "cannot read property of undefined" errors.

Final thoughts

Colorful confetti scattered over the word 'Finally' symbolizing celebration or achievement.

Typing async code in TypeScript is about setting boundaries. You’re telling the compiler, "This is what I expect, and this is how I handle it when things go wrong."

I still occasionally get frustrated when a complex API response takes longer to type than to write, but I've realized that the time spent typing is just the cost of doing business. If I had to do it all over again, I’d stop trying to be clever with any much sooner. Just define the interface, handle the errors, and let the compiler work for you.

Back to Blog

Similar Posts

Detailed view of code and file structure in a software development environment.
TypeScriptJavaScriptJune 20, 20264 min read

TypeScript narrowing: How to make the compiler trust your code

TypeScript narrowing is the key to writing type-safe code without constant casting. Learn how to guide the compiler through your logic for cleaner builds.

Read more
A vibrant workspace showing computer monitors with code, keyboard, and tech accessories.
TypeScriptJavaScriptJune 20, 20264 min read

Generics in TypeScript that actually pay off for your codebase

Generics in TypeScript can feel like an academic hurdle, but they pay off when you use them to enforce type safety in API calls and reusable components.

Read more
Typewritten note with 'I love you' on vintage paper background, evoking nostalgia.
TypeScriptJavaScriptJune 20, 20264 min read

TypeScript utility types you will reach for weekly

TypeScript utility types can save you hours of boilerplate code. Learn the essential tools I use weekly to keep my production projects clean and type-safe.

Read more