Async and Error Types

Back

Loading concept...

🚀 TypeScript: Async and Error Types

The Pizza Delivery Story 🍕

Imagine you order a pizza. You don’t stand at the door waiting the whole time—you do other things while the pizza is being made and delivered. When the doorbell rings, you get your pizza (success!) or maybe the driver says “Sorry, we ran out of cheese” (error!).

This is exactly how async code works in TypeScript. Let’s learn how to tell TypeScript what kind of pizza (data) we’re expecting, and what to do when things go wrong!


🎯 Promise Typing

What is a Promise?

A Promise is like a pizza order receipt. It says: “I promise to give you something later.”

// A promise that will give us a string
const myPromise: Promise<string> =
  new Promise((resolve) => {
    resolve("Hello!");
  });

The Magic Formula

Promise<WhatYouWillGet>

Simple Example:

  • Promise<string> → You’ll get text
  • Promise<number> → You’ll get a number
  • Promise<User> → You’ll get a User object

Real Example: Fetching a User

// Define what a User looks like
interface User {
  id: number;
  name: string;
  email: string;
}

// This function PROMISES to give us a User
async function getUser(): Promise<User> {
  const user: User = {
    id: 1,
    name: "Sam",
    email: "sam@example.com"
  };
  return user;
}

Why Type Promises?

Without typing:

// ❌ TypeScript doesn't know what's inside
async function getUser() {
  return someData; // What is this???
}

With typing:

// ✅ Crystal clear!
async function getUser(): Promise<User> {
  return userData; // Must be a User!
}
graph TD A["📝 Call async function"] --> B["🎫 Get a Promise"] B --> C{⏳ Waiting...} C --> D["✅ resolve: Get your data!"] C --> E["❌ reject: Get an error!"]

🛡️ Error Handling Types

The Problem: Errors are Sneaky

In JavaScript, anything can go wrong. TypeScript helps us catch problems!

Custom Error Types

Create your own error types to be super specific:

// Define specific error types
class NetworkError extends Error {
  constructor(
    message: string,
    public statusCode: number
  ) {
    super(message);
    this.name = "NetworkError";
  }
}

class ValidationError extends Error {
  constructor(
    message: string,
    public field: string
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

Using Error Types

function validateEmail(email: string): void {
  if (!email.includes("@")) {
    throw new ValidationError(
      "Invalid email",
      "email"
    );
  }
}

try {
  validateEmail("bad-email");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Fix the ${error.field} field`);
  }
}

The Result Pattern 🎁

Instead of throwing errors, return a “result box” that either has your data OR an error:

// Success or Failure - clearly typed!
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

function divide(
  a: number,
  b: number
): Result<number> {
  if (b === 0) {
    return {
      success: false,
      error: new Error("Can't divide by zero!")
    };
  }
  return { success: true, data: a / b };
}

// Using it
const result = divide(10, 2);
if (result.success) {
  console.log(result.data); // 5 ✅
} else {
  console.log(result.error.message); // ❌
}
graph TD A["🎯 Function runs"] --> B{Did it work?} B -->|Yes| C["✅ &#123;success: true, data: ...&#125;"] B -->|No| D["❌ &#123;success: false, error: ...&#125;"]

🌐 Typing API Responses

The Real World

When you call an API, you get JSON back. TypeScript needs to know what shape that JSON has!

Step 1: Define Your Types

// What the API returns
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

Step 2: Type Your Fetch

async function fetchProducts():
  Promise<ApiResponse<Product[]>> {

  const response = await fetch("/api/products");
  const json = await response.json();

  // Now TypeScript knows json is ApiResponse<Product[]>
  return json as ApiResponse<Product[]>;
}

Step 3: Handle All Cases

interface ApiError {
  code: string;
  message: string;
}

type ApiResult<T> =
  | { ok: true; data: ApiResponse<T> }
  | { ok: false; error: ApiError };

async function safeApiFetch<T>(
  url: string
): Promise<ApiResult<T>> {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      return {
        ok: false,
        error: {
          code: String(response.status),
          message: "Request failed"
        }
      };
    }

    const data = await response.json();
    return { ok: true, data };

  } catch (e) {
    return {
      ok: false,
      error: {
        code: "NETWORK_ERROR",
        message: "Could not connect"
      }
    };
  }
}

Using the Safe Fetch

const result = await safeApiFetch<Product[]>(
  "/api/products"
);

if (result.ok) {
  // TypeScript knows: result.data exists
  console.log(result.data.data); // Product[]
} else {
  // TypeScript knows: result.error exists
  console.log(result.error.message);
}

✨ The using Keyword (NEW!)

What is using?

The using keyword is like a responsible friend who always cleans up after a party! 🎉

When you’re done with something (like a file or database connection), using makes sure it gets closed properly.

The Old Way (Messy 😰)

async function readFile() {
  const file = await openFile("data.txt");
  try {
    const content = await file.read();
    return content;
  } finally {
    await file.close(); // Don't forget!
  }
}

The New Way (Clean 😊)

async function readFile() {
  await using file = await openFile("data.txt");
  const content = await file.read();
  return content;
  // file.close() happens automatically! ✨
}

How to Make It Work

Your object needs a special cleanup method:

// For sync cleanup
interface Disposable {
  [Symbol.dispose](): void;
}

// For async cleanup
interface AsyncDisposable {
  [Symbol.asyncDispose](): Promise<void>;
}

Real Example: Database Connection

class DatabaseConnection
  implements AsyncDisposable {

  private connected = false;

  async connect() {
    console.log("🔌 Connecting...");
    this.connected = true;
  }

  async query(sql: string) {
    return `Results for: ${sql}`;
  }

  // This runs automatically when done!
  async [Symbol.asyncDispose]() {
    console.log("👋 Disconnecting...");
    this.connected = false;
  }
}

// Usage
async function getData() {
  await using db = new DatabaseConnection();
  await db.connect();
  const data = await db.query("SELECT *");
  return data;
  // 👋 Disconnecting... happens automatically!
}

When to Use using

graph TD A["Need to use a resource?"] --> B{Does it need cleanup?} B -->|Yes| C["Use &&#35;39;using&&#35;39; keyword!"] B -->|No| D["Regular variable is fine"] C --> E["File handles"] C --> F["Database connections"] C --> G["Network sockets"] C --> H["Locks and semaphores"]

🎯 Quick Summary

Concept What It Does Key Syntax
Promise Typing Tells what a promise returns Promise<User>
Error Types Custom errors with extra info class MyError extends Error
Result Pattern Success or error, never throw Result<T, E>
API Response Types Shape of API data ApiResponse<T>
using keyword Auto-cleanup resources using x = ...

🧠 Remember This!

  1. Always type your Promises → Know what you’re getting
  2. Create custom error classes → Be specific about what went wrong
  3. Use the Result pattern → Never let errors surprise you
  4. Type your API responses → Trust the data you receive
  5. Use using for cleanup → Never forget to close things

You’ve got this! TypeScript’s type system is your friend, making sure your async code is safe and predictable. 🚀

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.