Asynchronous Programming and Promises

1. Promise<T> Types and async/await Syntax

Feature Syntax Description Return Type
Promise<T> Promise<string> Generic type representing async operation result T when awaited
async Function async function() {} Function that always returns Promise Promise<ReturnType>
await await promise Wait for Promise to resolve, extract value Unwrapped T from Promise<T>
Promise.resolve Promise.resolve(value) Create resolved Promise Promise<T>
Promise.reject Promise.reject(error) Create rejected Promise Promise<never>
Promise.all Promise.all([p1, p2]) Wait for all Promises, return array Promise<[T1, T2]>
Promise.race Promise.race([p1, p2]) Return first settled Promise Promise<T1 | T2>

Example: Basic Promise types

// Promise type annotation
const promise1: Promise<string> = Promise.resolve('hello');
const promise2: Promise<number> = Promise.resolve(42);

// Promise that might reject
function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                resolve('Data loaded');
            } else {
                reject(new Error('Failed to load'));
            }
        }, 1000);
    });
}

// Using Promises with then/catch
fetchData()
    .then(data => console.log(data))  // data: string
    .catch(error => console.error(error));  // error: any

// Promise chaining
function getUser(id: string): Promise<User> {
    return fetch(`/api/users/${id}`)
        .then(response => response.json())
        .then(data => data as User);
}

// Promise constructor typing
const customPromise = new Promise<number>((resolve, reject) => {
    resolve(42);  // ✓ OK: number
    resolve('hello');  // ✗ Error: string not assignable to number
});

Example: async/await syntax

// async function automatically wraps return in Promise
async function getName(): Promise<string> {
    return 'Alice';  // Actually returns Promise<string>
}

async function getAge(): Promise<number> {
    return 25;
}

// await unwraps Promise
async function displayUser() {
    const name = await getName();  // Type: string (not Promise<string>)
    const age = await getAge();    // Type: number
    
    console.log(`${name} is ${age} years old`);
}

// Multiple awaits
async function loadData() {
    const users = await fetchUsers();      // User[]
    const posts = await fetchPosts();      // Post[]
    const comments = await fetchComments(); // Comment[]
    
    return { users, posts, comments };
}

// await with non-Promise values (no-op)
async function demo() {
    const x = await 42;  // Type: number (42 wrapped in Promise.resolve)
    const y = await 'hello';  // Type: string
}

Example: Promise combinators

// Promise.all - parallel execution
async function loadMultiple() {
    const [users, posts, settings] = await Promise.all([
        fetchUsers(),    // Promise<User[]>
        fetchPosts(),    // Promise<Post[]>
        fetchSettings()  // Promise<Settings>
    ]);
    // Type: [User[], Post[], Settings]
    
    return { users, posts, settings };
}

// Promise.allSettled - all results (success or failure)
async function tryLoadAll() {
    const results = await Promise.allSettled([
        fetchUsers(),
        fetchPosts(),
        Promise.reject('error')
    ]);
    
    // Type: PromiseSettledResult<User[] | Post[] | never>[]
    results.forEach(result => {
        if (result.status === 'fulfilled') {
            console.log('Success:', result.value);
        } else {
            console.log('Failed:', result.reason);
        }
    });
}

// Promise.race - first to complete
async function timeout<T>(
    promise: Promise<T>,
    ms: number
): Promise<T> {
    const timeoutPromise = new Promise<never>((_, reject) => {
        setTimeout(() => reject(new Error('Timeout')), ms);
    });
    
    return Promise.race([promise, timeoutPromise]);
}

// Promise.any - first successful
async function loadFromMirrors(urls: string[]): Promise<Response> {
    const promises = urls.map(url => fetch(url));
    return Promise.any(promises);  // First successful response
}

2. Typing Async Functions and Return Types

Pattern Return Type Description Behavior
async function Promise<T> Always wraps return value in Promise Automatic Promise wrapping
async () => T Promise<T> Arrow function version Same as async function
Return Promise<T> Promise<T> Explicit Promise return (no unwrapping) Not double-wrapped
Return void Promise<void> Async function with no return Returns Promise<undefined>
Throw in async Promise<never> Always rejects Rejected Promise

Example: Async function return types

// Inferred return type: Promise<string>
async function getString() {
    return 'hello';
}

// Explicit return type
async function getNumber(): Promise<number> {
    return 42;
}

// Returning Promise<T> from async (not double-wrapped)
async function getUser(): Promise<User> {
    // Returning Promise<User> doesn't create Promise<Promise<User>>
    return fetch('/api/user').then(r => r.json());
}

// void async function
async function saveData(data: string): Promise<void> {
    await fetch('/api/save', {
        method: 'POST',
        body: data
    });
    // No return statement, returns Promise<void>
}

// Multiple return paths
async function conditional(flag: boolean): Promise<string | number> {
    if (flag) {
        return 'text';   // string
    }
    return 42;          // number
}

// Early return
async function findUser(id: string): Promise<User | null> {
    if (!id) return null;  // Early return
    
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) return null;
    
    return response.json();
}

Example: Generic async functions

// Generic async function
async function fetchJson<T>(url: string): Promise<T> {
    const response = await fetch(url);
    return response.json();
}

// Usage with type parameter
const user = await fetchJson<User>('/api/user');
const posts = await fetchJson<Post[]>('/api/posts');

// Constrained generic
async function retry<T>(
    fn: () => Promise<T>,
    attempts: number
): Promise<T> {
    for (let i = 0; i < attempts; i++) {
        try {
            return await fn();
        } catch (error) {
            if (i === attempts - 1) throw error;
        }
    }
    throw new Error('All attempts failed');
}

// Multiple generic parameters
async function transform<T, U>(
    data: T,
    transformer: (item: T) => Promise<U>
): Promise<U> {
    return await transformer(data);
}

// Async method in class
class UserService {
    async getUser(id: string): Promise<User> {
        const response = await fetch(`/api/users/${id}`);
        return response.json();
    }
    
    async deleteUser(id: string): Promise<void> {
        await fetch(`/api/users/${id}`, { method: 'DELETE' });
    }
}

Example: Async function types

// Function type for async functions
type AsyncFunction<T> = () => Promise<T>;

const loader: AsyncFunction<string> = async () => {
    return 'loaded';
};

// Async function with parameters
type FetchFunction<T> = (id: string) => Promise<T>;

const fetchUser: FetchFunction<User> = async (id) => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
};

// Interface with async methods
interface ApiClient {
    get<T>(url: string): Promise<T>;
    post<T>(url: string, data: any): Promise<T>;
    delete(url: string): Promise<void>;
}

const client: ApiClient = {
    async get(url) {
        const response = await fetch(url);
        return response.json();
    },
    async post(url, data) {
        const response = await fetch(url, {
            method: 'POST',
            body: JSON.stringify(data)
        });
        return response.json();
    },
    async delete(url) {
        await fetch(url, { method: 'DELETE' });
    }
};

3. Promise Utility Types (Awaited<T>)

Utility Type Description Example Use Case
Awaited<T> Unwrap Promise type recursively Awaited<Promise<string>> = string Extract Promise result type
ReturnType with async Get function return type ReturnType<typeof fn> Promise<T> for async functions
Awaited with nested Unwrap deeply nested Promises Awaited<Promise<Promise<T>>> Flatten nested Promises
PromiseSettledResult Type for Promise.allSettled fulfilled | rejected Handle settled results

Example: Awaited utility type

// Extract type from Promise
type Result1 = Awaited<Promise<string>>;  // string
type Result2 = Awaited<Promise<number[]>>;  // number[]

// Works with nested Promises
type Nested = Promise<Promise<Promise<User>>>;
type Unwrapped = Awaited<Nested>;  // User

// Extract return type from async function
async function fetchUser(): Promise<User> {
    const response = await fetch('/api/user');
    return response.json();
}

type UserType = Awaited<ReturnType<typeof fetchUser>>;
// Type: User (not Promise<User>)

// With union types
type AsyncData = Promise<string> | Promise<number>;
type UnwrappedData = Awaited<AsyncData>;  // string | number

// Non-Promise types pass through
type NotPromise = Awaited<string>;  // string
type Mixed = Awaited<string | Promise<number>>;  // string | number

Example: Practical Awaited usage

// Type-safe async result handling
async function loadData() {
    return {
        users: await fetchUsers(),
        posts: await fetchPosts(),
        settings: await fetchSettings()
    };
}

type LoadedData = Awaited<ReturnType<typeof loadData>>;
// Type: {
//   users: User[];
//   posts: Post[];
//   settings: Settings;
// }

// Extract types from Promise arrays
type UserPromise = Promise<User>;
type UsersArray = UserPromise[];

type ExtractedUser = Awaited<UserPromise>;  // User
type ExtractedUsers = Awaited<UsersArray[number]>;  // User

// Helper to unwrap all properties
type UnwrapPromises<T> = {
    [K in keyof T]: Awaited<T[K]>;
};

interface AsyncState {
    user: Promise<User>;
    posts: Promise<Post[]>;
    count: number;  // Already not a Promise
}

type SyncState = UnwrapPromises<AsyncState>;
// Type: {
//   user: User;
//   posts: Post[];
//   count: number;
// }

// Type inference with Promise.all
const promises = [
    Promise.resolve('text'),
    Promise.resolve(42),
    Promise.resolve(true)
] as const;

type AllResults = Awaited<typeof Promise.all<typeof promises>>;
// Type: [string, number, boolean]

Example: PromiseSettledResult types

// Type for Promise.allSettled results
type SettledUser = PromiseSettledResult<User>;
// Type: PromiseFulfilledResult<User> | PromiseRejectedResult

// Type-safe handling of settled results
async function loadAllUsers(ids: string[]) {
    const promises = ids.map(id => fetchUser(id));
    const results = await Promise.allSettled(promises);
    
    // Type: PromiseSettledResult<User>[]
    const users: User[] = [];
    const errors: Error[] = [];
    
    results.forEach(result => {
        if (result.status === 'fulfilled') {
            // result.value is User
            users.push(result.value);
        } else {
            // result.reason is any (error)
            errors.push(result.reason);
        }
    });
    
    return { users, errors };
}

// Custom type guard for fulfilled results
function isFulfilled<T>(
    result: PromiseSettledResult<T>
): result is PromiseFulfilledResult<T> {
    return result.status === 'fulfilled';
}

async function processResults() {
    const results = await Promise.allSettled([
        fetchUser('1'),
        fetchUser('2'),
        fetchUser('3')
    ]);
    
    const users = results
        .filter(isFulfilled)
        .map(result => result.value);  // Type: User[]
}

4. Error Handling in Async Code

Pattern Syntax Description Best For
try/catch try { await } catch (e) {} Catch errors in async functions Synchronous error handling style
.catch() promise.catch(handler) Handle Promise rejection Promise chains
Error Type catch (e: unknown) Error parameter is unknown in TS Type-safe error handling
Result Type { success: boolean; data?: T; error?: E } Explicit success/failure types Functional error handling

Example: try/catch with async/await

// Basic try/catch
async function loadUser(id: string): Promise<User | null> {
    try {
        const response = await fetch(`/api/users/${id}`);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return await response.json();
    } catch (error) {
        // error is unknown (not any in strict mode)
        console.error('Failed to load user:', error);
        return null;
    }
}

// Type-safe error handling
async function safeLoad(id: string): Promise<User> {
    try {
        return await loadUser(id);
    } catch (error) {
        // Narrow error type
        if (error instanceof Error) {
            console.error(error.message);
        } else {
            console.error('Unknown error', error);
        }
        throw error;  // Re-throw after logging
    }
}

// Multiple try/catch blocks
async function complexOperation() {
    let user: User | null = null;
    let posts: Post[] = [];
    
    try {
        user = await fetchUser('123');
    } catch (error) {
        console.error('User fetch failed:', error);
    }
    
    try {
        posts = await fetchPosts();
    } catch (error) {
        console.error('Posts fetch failed:', error);
    }
    
    return { user, posts };
}

Example: Result type pattern

// Result type for explicit error handling
type Result<T, E = Error> = 
    | { success: true; data: T }
    | { success: false; error: E };

async function fetchUserSafe(id: string): Promise<Result<User>> {
    try {
        const response = await fetch(`/api/users/${id}`);
        
        if (!response.ok) {
            return {
                success: false,
                error: new Error(`HTTP ${response.status}`)
            };
        }
        
        const data = await response.json();
        return { success: true, data };
    } catch (error) {
        return {
            success: false,
            error: error instanceof Error ? error : new Error('Unknown error')
        };
    }
}

// Usage with type narrowing
async function displayUser(id: string) {
    const result = await fetchUserSafe(id);
    
    if (result.success) {
        // result.data is User
        console.log(result.data.name);
    } else {
        // result.error is Error
        console.error(result.error.message);
    }
}

// Either type (functional approach)
type Either<L, R> = 
    | { type: 'left'; value: L }
    | { type: 'right'; value: R };

async function tryFetch<T>(url: string): Promise<Either<Error, T>> {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return { type: 'right', value: data };
    } catch (error) {
        return {
            type: 'left',
            value: error instanceof Error ? error : new Error('Unknown')
        };
    }
}

Example: Error types and custom errors

// Custom error classes
class ApiError extends Error {
    constructor(
        message: string,
        public statusCode: number,
        public response?: any
    ) {
        super(message);
        this.name = 'ApiError';
    }
}

class NetworkError extends Error {
    constructor(message: string) {
        super(message);
        this.name = 'NetworkError';
    }
}

// Type-safe error throwing
async function fetchData(url: string): Promise<any> {
    let response: Response;
    
    try {
        response = await fetch(url);
    } catch (error) {
        throw new NetworkError('Network request failed');
    }
    
    if (!response.ok) {
        throw new ApiError(
            'API request failed',
            response.status,
            await response.text()
        );
    }
    
    return response.json();
}

// Discriminated error handling
async function handleRequest(url: string) {
    try {
        const data = await fetchData(url);
        return data;
    } catch (error) {
        if (error instanceof ApiError) {
            console.error(`API Error ${error.statusCode}:`, error.message);
            if (error.statusCode === 404) {
                return null;
            }
        } else if (error instanceof NetworkError) {
            console.error('Network error:', error.message);
            // Retry logic here
        } else {
            console.error('Unknown error:', error);
        }
        throw error;
    }
}

// Error union type
type FetchError = ApiError | NetworkError | Error;

async function safeFetch(url: string): Promise<Result<any, FetchError>> {
    try {
        const data = await fetchData(url);
        return { success: true, data };
    } catch (error) {
        return {
            success: false,
            error: error as FetchError
        };
    }
}

5. Generator Functions and AsyncGenerator Types

Type Syntax Description Use Case
Generator<T, R, N> function* gen() Sync generator - yields values Lazy sequences, iterators
AsyncGenerator<T, R, N> async function* gen() Async generator - yields Promises Streaming data, pagination
yield yield value Produce value from generator Emit next value
yield* yield* iterable Delegate to another generator Compose generators

Example: Generator functions

// Basic generator
function* numberGenerator(): Generator<number, void, unknown> {
    yield 1;
    yield 2;
    yield 3;
}

// Usage
const gen = numberGenerator();
console.log(gen.next().value);  // 1
console.log(gen.next().value);  // 2
console.log(gen.next().value);  // 3
console.log(gen.next().done);   // true

// Infinite generator
function* fibonacci(): Generator<number> {
    let [a, b] = [0, 1];
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}

// Take first n values
function* take<T>(n: number, iterable: Iterable<T>): Generator<T> {
    let count = 0;
    for (const value of iterable) {
        if (count++ >= n) break;
        yield value;
    }
}

const firstTen = [...take(10, fibonacci())];
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// Generator with return value
function* withReturn(): Generator<number, string, unknown> {
    yield 1;
    yield 2;
    return 'done';  // Return value different from yield type
}

const result = withReturn();
console.log(result.next());  // { value: 1, done: false }
console.log(result.next());  // { value: 2, done: false }
console.log(result.next());  // { value: 'done', done: true }

Example: Async generators

// Async generator for pagination
async function* fetchPages(
    baseUrl: string
): AsyncGenerator<User[], void, unknown> {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        const response = await fetch(`${baseUrl}?page=${page}`);
        const data = await response.json();
        
        if (data.users.length === 0) {
            hasMore = false;
        } else {
            yield data.users;
            page++;
        }
    }
}

// Usage with for await...of
async function loadAllUsers() {
    const allUsers: User[] = [];
    
    for await (const users of fetchPages('/api/users')) {
        allUsers.push(...users);
        console.log(`Loaded ${users.length} users`);
    }
    
    return allUsers;
}

// Stream processing
async function* processStream<T, U>(
    source: AsyncIterable<T>,
    transform: (item: T) => Promise<U>
): AsyncGenerator<U> {
    for await (const item of source) {
        yield await transform(item);
    }
}

// Async generator with error handling
async function* fetchWithRetry(
    urls: string[]
): AsyncGenerator<Response, void, unknown> {
    for (const url of urls) {
        let retries = 3;
        while (retries > 0) {
            try {
                const response = await fetch(url);
                yield response;
                break;
            } catch (error) {
                retries--;
                if (retries === 0) throw error;
                await new Promise(resolve => setTimeout(resolve, 1000));
            }
        }
    }
}

Example: Generator type parameters

// Generator<YieldType, ReturnType, NextType>

// NextType - type of value sent via next()
function* counter(): Generator<number, void, number> {
    let count = 0;
    while (true) {
        const increment = yield count;  // increment is number | undefined
        count += increment ?? 1;
    }
}

const gen = counter();
console.log(gen.next());      // { value: 0, done: false }
console.log(gen.next(5));     // { value: 5, done: false }
console.log(gen.next(10));    // { value: 15, done: false }

// Generic async generator
async function* map<T, U>(
    source: AsyncIterable<T>,
    fn: (item: T) => U | Promise<U>
): AsyncGenerator<U, void, unknown> {
    for await (const item of source) {
        yield await fn(item);
    }
}

// Filter async generator
async function* filter<T>(
    source: AsyncIterable<T>,
    predicate: (item: T) => boolean | Promise<boolean>
): AsyncGenerator<T, void, unknown> {
    for await (const item of source) {
        if (await predicate(item)) {
            yield item;
        }
    }
}

// Compose async generators
async function* pipeline<T>(source: AsyncIterable<T>) {
    yield* filter(
        map(source, async (x: T) => transform(x)),
        async (x) => await validate(x)
    );
}

6. Callback Types and Event Handler Typing

Pattern Type Description Common Usage
Callback (arg: T) => void Function called with result Node.js style callbacks
Error-first Callback (err: Error | null, data?: T) => void Node.js convention Async operations
Event Handler (event: Event) => void DOM event handler Browser events
Generic Callback <T>(data: T) => void Type-safe callbacks Custom events, streams

Example: Callback function types

// Simple callback
type Callback<T> = (data: T) => void;

function fetchData(url: string, callback: Callback<string>) {
    fetch(url)
        .then(response => response.text())
        .then(data => callback(data));
}

// Error-first callback (Node.js style)
type ErrorCallback<T> = (error: Error | null, data?: T) => void;

function readFile(path: string, callback: ErrorCallback<string>) {
    try {
        // Read file logic
        const data = 'file content';
        callback(null, data);
    } catch (error) {
        callback(error as Error);
    }
}

// Usage
readFile('file.txt', (error, data) => {
    if (error) {
        console.error(error);
        return;
    }
    console.log(data);  // Type: string | undefined
});

// Callback with multiple parameters
type MultiCallback = (success: boolean, data: User, meta: MetaData) => void;

function loadUser(id: string, callback: MultiCallback) {
    // Load user logic
    callback(true, user, metadata);
}

Example: Event handler types

// DOM event handlers
type ClickHandler = (event: MouseEvent) => void;
type KeyHandler = (event: KeyboardEvent) => void;
type FormHandler = (event: FormEvent) => void;

// Button click handler
const handleClick: ClickHandler = (event) => {
    console.log(event.clientX, event.clientY);
    event.preventDefault();
};

// Generic event handler
type EventHandler<T extends Event> = (event: T) => void;

const handleSubmit: EventHandler<FormEvent> = (event) => {
    event.preventDefault();
    const form = event.currentTarget as HTMLFormElement;
    // Process form
};

// React-style event handlers
type ReactChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => void;

const handleChange: ReactChangeHandler = (event) => {
    const value = event.target.value;
    console.log(value);
};

// Custom event emitter
class EventEmitter<T> {
    private listeners: Array<(data: T) => void> = [];
    
    on(listener: (data: T) => void): void {
        this.listeners.push(listener);
    }
    
    emit(data: T): void {
        this.listeners.forEach(listener => listener(data));
    }
}

// Usage
const emitter = new EventEmitter<User>();
emitter.on((user) => {
    console.log(user.name);  // Type-safe: user is User
});

Example: Promisify callbacks

// Convert callback-based function to Promise
function promisify<T>(
    fn: (callback: ErrorCallback<T>) => void
): Promise<T> {
    return new Promise((resolve, reject) => {
        fn((error, data) => {
            if (error) reject(error);
            else resolve(data!);
        });
    });
}

// Generic promisify for Node.js functions
function promisifyNode<T>(
    fn: (...args: any[]) => void
): (...args: any[]) => Promise<T> {
    return function(...args) {
        return new Promise((resolve, reject) => {
            fn(...args, (error: Error | null, data?: T) => {
                if (error) reject(error);
                else resolve(data!);
            });
        });
    };
}

// Usage
function oldStyleRead(path: string, callback: ErrorCallback<string>) {
    // Old callback-based code
    callback(null, 'content');
}

const readAsync = promisify(oldStyleRead);

// Now can use with async/await
async function example() {
    const content = await readAsync('file.txt');
    console.log(content);
}

// Observable-style callbacks
interface Observer<T> {
    next: (value: T) => void;
    error: (error: Error) => void;
    complete: () => void;
}

function subscribe<T>(observer: Observer<T>) {
    try {
        observer.next(value);
        observer.complete();
    } catch (error) {
        observer.error(error as Error);
    }
}
Note: Modern async patterns:
  • Prefer async/await over callbacks for readability
  • Use Promise.all for parallel async operations
  • Consider async generators for streaming data
  • Use Result types for explicit error handling
  • Enable strictNullChecks for better async type safety
Warning: Common async pitfalls:
  • Forgetting await - returns Promise instead of value
  • Error in catch is unknown, not any - requires type narrowing
  • Parallel requests with sequential await - use Promise.all
  • Unhandled Promise rejections - always handle with catch or try/catch
  • Type narrowing doesn't persist across await boundaries

Asynchronous Programming Summary

  • Promise<T> - Generic type for async operations with async/await syntax
  • Async functions - Always return Promise<T>, automatic Promise wrapping
  • Awaited<T> - Utility type to unwrap Promise types recursively
  • Error handling - try/catch with unknown type, Result types for explicit errors
  • Generators - Generator<T> for sync, AsyncGenerator<T> for async iteration
  • Callbacks - Type-safe callbacks with error-first pattern and event handlers