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
strictNullChecksfor better async type safety
Warning: Common async pitfalls:
- Forgetting
await- returns Promise instead of value - Error in catch is
unknown, notany- 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