TypeScript Promises

A Promise is an object that represents the eventual result of an asynchronous operation. When the operation finishes, the Promise either resolves with a value (success) or rejects with an error (failure). TypeScript types Promises with a generic parameter — Promise<T> — so the resolved value always has a known type, eliminating guesswork in async code.

What a Promise Looks Like in Real Life

REAL WORLD:
  You order food online
       │
       ├── Promise created: "Your food will arrive"
       │
       ├── PENDING   → kitchen is cooking (in progress)
       │
       ├── FULFILLED → food delivered! (resolved with value)
       │
       └── REJECTED  → restaurant cancelled (rejected with reason)

IN CODE:
  let order = new Promise((resolve, reject) => {
      // async work happens here
  });

Promise States

StateMeaningWhat Happens Next
PendingOperation is runningMoves to fulfilled or rejected
FulfilledOperation succeeded.then() callback runs with the value
RejectedOperation failed.catch() callback runs with the error

Creating a Typed Promise

// Promise<string> means the resolved value will be a string
function fetchWelcomeMessage(userId: number): Promise<string> {
    return new Promise((resolve, reject) => {
        if (userId <= 0) {
            reject(new Error("Invalid user ID"));
            return;
        }
        // Simulate network delay
        setTimeout(() => {
            resolve(`Welcome back, User ${userId}!`);
        }, 1000);
    });
}

fetchWelcomeMessage(5)
    .then((message: string) => {
        console.log(message);  // Welcome back, User 5! (after 1 sec)
    })
    .catch((error: Error) => {
        console.log("Error:", error.message);
    });

Promise Chaining with Types

Each .then() can transform the value and pass it to the next .then(). TypeScript tracks the type at each stage.

CHAIN FLOW:
  fetchId()          → Promise<number>
      .then(fetchUser)   → Promise<User>
      .then(formatUser)  → Promise<string>
      .catch(handleErr)  → catches any error above

function fetchId(): Promise<number> {
    return Promise.resolve(42);
}

function fetchUser(id: number): Promise<{ name: string; email: string }> {
    return Promise.resolve({ name: "Divya", email: "divya@mail.com" });
}

function formatUser(user: { name: string; email: string }): string {
    return `${user.name} (${user.email})`;
}

fetchId()
    .then(fetchUser)
    .then(formatUser)
    .then((formatted: string) => {
        console.log(formatted);   // Divya (divya@mail.com)
    })
    .catch((error: Error) => {
        console.log("Pipeline failed:", error.message);
    });

Promise.resolve and Promise.reject

// Create an immediately resolved promise
let success: Promise<number> = Promise.resolve(100);
success.then(n => console.log(n));  // 100

// Create an immediately rejected promise
let failure: Promise<never> = Promise.reject(new Error("Immediate failure"));
failure.catch(e => console.log(e.message));  // Immediate failure

Promise.all — Wait for All

DIAGRAM:
  Promise A ────────────── resolve(userA)
  Promise B ──── resolve(userB)
  Promise C ─────────────────── resolve(userC)
                                    │
                               all() resolves
                               with [userA, userB, userC]

interface CourseInfo {
    title: string;
    students: number;
}

async function loadCourses(): Promise<void> {
    let courseIds = [1, 2, 3];

    let coursePromises: Promise<CourseInfo>[] = courseIds.map(id =>
        fetchCourse(id)
    );

    let courses: CourseInfo[] = await Promise.all(coursePromises);
    courses.forEach(c => console.log(`${c.title}: ${c.students} students`));
}

function fetchCourse(id: number): Promise<CourseInfo> {
    let courses: CourseInfo[] = [
        { title: "TypeScript", students: 1200 },
        { title: "React",      students: 3500 },
        { title: "Node.js",    students: 2100 }
    ];
    return Promise.resolve(courses[id - 1]);
}

Promise.race — First One Wins

// Resolves or rejects as soon as the FIRST promise settles
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
    let timeout = new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
    );
    return Promise.race([promise, timeout]);
}

async function main(): Promise<void> {
    try {
        let result = await withTimeout(fetchUser(1), 2000);
        console.log("Got result in time:", result);
    } catch (error: unknown) {
        if (error instanceof Error) {
            console.log(error.message);   // Timed out after 2000ms
        }
    }
}

Typed Promise Utility

// Generic retry function — retries a failing promise up to N times
async function retry<T>(
    fn: () => Promise<T>,
    attempts: number
): Promise<T> {
    let lastError: Error = new Error("Unknown error");

    for (let i = 0; i < attempts; i++) {
        try {
            return await fn();
        } catch (error: unknown) {
            if (error instanceof Error) {
                lastError = error;
                console.log(`Attempt ${i + 1} failed: ${error.message}`);
            }
        }
    }
    throw lastError;
}

let callCount = 0;

async function unstableApi(): Promise<string> {
    callCount++;
    if (callCount < 3) throw new Error("Server overloaded");
    return "Data loaded successfully";
}

retry(unstableApi, 5).then(result => {
    console.log(result);  // Data loaded successfully (on 3rd attempt)
});

Promises vs async/await

AspectPromises (.then/.catch)async/await
ReadabilityGets complex with chainingReads like synchronous code
Error handling.catch() at endtry/catch blocks
Parallel executionPromise.all()await Promise.all()
Race conditionPromise.race()await Promise.race()
Under the hoodFoundationSyntax sugar over Promises

Both work together. async/await is built on Promises. Knowing how Promises work helps write better async/await code and debug it effectively.

Leave a Comment

Your email address will not be published. Required fields are marked *