C
TypeScript/Async/Lesson 04

async / await — Annotating the Return Type Lets Callers Infer Automatically

30 min·theory
This chapter
4/7
TypeScript

async / await — Annotating the Return Type Lets Callers Infer Automatically

💡 Why Should You Learn This? — An async Function's Signature Is a Contract

🎯 With JS async functions, you have to read through the entire function body to know what they return.
💼 With TS async functions, if you pin `Promise` in the signature, callers can use them safely without reading the implementation.
The result type of `await` is inferred automatically — a type you pin once flows through to the end of the await chain.
🔗 The `err` in try/catch has been `unknown` since TS 4.4+. You must narrow it with something like `instanceof Error` before accessing `.message`.
🏢 실무에서는
Next.js Server Components, Server Actions, and API Route handlers are all async functions. When you explicitly annotate the return type, every client and server location that imports and uses that function gets parameter and return-value hints from the IDE. Without annotation, forced casts like `as User` accumulate at every call site.

async Function = Always Returns a Promise, await = Unwrapping It

1. An async function's return type is automatically wrapped in Promise

ts
async function getUser(): Promise<User> {
  return { id: 1, name: 'Hong Gil-dong' };
  //       ↑ Just return a User object
  //         The actual return type is automatically Promise<User>
}
  • A function marked async always returns a Promise.
  • The return type must be in the form Promise<...> (specifying any other type in the signature causes a compile error).
  • Returning return value in the body automatically wraps it as Promise.resolve(value).
  • Throwing throw err in the body automatically becomes Promise.reject(err).

2. The result of await is inferred automatically

ts
async function loadAll() {
  const user: User = await getUser();          // Inference OK
  const orders: Order[] = await getOrders(user.id); // Chained inference
  return { user, orders };
}
// The return type of loadAll is automatically Promise<{ user: User; orders: Order[] }>

3. The err in catch is unknown (TS 4.4+)

ts
try {
  await dangerousAction();
} catch (err) {
  // The type of err is unknown — cannot access .message directly
  // err.message; ❌ Object is of type 'unknown'

  if (err instanceof Error) {
    console.log(err.message); // ✅ Narrowed
  } else {
    console.log('Unknown error:', err);
  }
}

4. A Promise-returning function and an async function are equivalent

ts
// The signatures of the two functions are identical
function a(): Promise<number> {
  return Promise.resolve(42);
}
async function b(): Promise<number> {
  return 42;
}
// Calls to a() and b() behave identically

Choose whichever you prefer, but if you want to use await inside the body, async is required.

💻 🅰️ JS Style — Opaque Return Type, err Is also any
// ❌ JS — Async function signatures are poor

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error('Failed');
  return res.json(); // Returns Promise<???>
}

async function main() {
  try {
    const user = await fetchUser(42);
    console.log(user.name);     // IDE doesn't know what user is
    console.log(user.naem);     // ❌ Typo but compiles
  } catch (err) {
    console.log(err.message);   // Accesses .message without guarantee that err is an Error
    // If 'string' was thrown, err.message would be undefined
  }
}
main();
💻 🅱️ TS Style — Signature + await Inference + Narrowing err
// ✅ TS — Explicit return type in signature + safe error handling

interface User {
  id: number;
  name: string;
  email: string;
}

class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError';
  }
}

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new ApiError('Failed', res.status);
  return res.json() as Promise<User>;
  // Or validate at runtime with zod etc. before returning
}

async function main(): Promise<void> {
  try {
    const user = await fetchUser(42);
    // user is inferred as User
    console.log(user.name);       // ✅ Autocomplete
    // console.log(user.naem);    // ❌ Compile error
    console.log(user.email);      // ✅ Exists on User
  } catch (err) {
    // err: unknown — cannot access .message directly
    if (err instanceof ApiError) {
      console.log(`API Error ${err.status}: ${err.message}`);
    } else if (err instanceof Error) {
      console.log('General Error:', err.message);
    } else {
      console.log('Unknown error:', err);
    }
  }
}
main();

💡 💡 async/await TypeScript Essentials — Top 5

1. Explicitly annotate the return type of async functions in the signature

ts
async function getUser(id: number): Promise<User> { ... }

The signature alone tells callers what to expect.

2. You can leave the await result to inference

ts
const user = await getUser(42); // user: User is automatically inferred

Annotating improves readability, but omitting is safe too.

3. err is unknown — narrow it before use

ts
catch (err) {
  if (err instanceof Error) console.log(err.message);
}

useUnknownInCatchVariables: true in tsconfig is the default for TS 4.4+ strict mode.

4. Don't double-wrap a Promise inside an async function

ts
// ❌ Double-wrapping
async function bad() { return Promise.resolve(42); }
// ✅ Just return
async function good() { return 42; }

5. Destructure the result of Promise.all as a tuple (already covered in the promise lesson)

ts
const [user, orders] = await Promise.all([getUser(1), getOrders(1)]);
// user: User, orders: Order[]

⚡ Try It Yourself — async/await

A runnable version with types stripped. Verify sequential processing with await and error handling with try/catch.
✏️ JS 코드
📟 Console output
▶ Press the Run button
⚠️ Runs in a browser sandbox — only console.log() is supported; alert/fetch are not.

Check Your Understanding

In TS 4.4+, why does `try { ... } catch (err) { console.log(err.message); }` produce a compile error?
💡 Since TS 4.4, `useUnknownInCatchVariables` became the default in strict mode, changing the type of `err` in `catch (err)` from `any` to `unknown`. Because `unknown` has no `.message` property, you must narrow the type first — for example with `if (err instanceof Error)`. This is the recommended approach as it is a safer default.
async / await — Return Type and err: unknown - TypeScript