Skip to main content
Maps over an iterable concurrently with configurable concurrency and resilience. Preserves order of output even if tasks finish out of order. Supports the Skip symbol to act as a combined map+filter.

Signature

async function map<T, R>(
  input: AsyncIterable<T> | Iterable<T> | Promise<Iterable<T>>,
  mapper: (item: T, index: number) => Promise<R | SkipSymbol> | R | SkipSymbol,
  options?: ConcurrencyOptions & ResilienceOptions,
): Promise<R[]>

Parameters

input
AsyncIterable<T> | Iterable<T> | Promise<Iterable<T>>
required
The iterable to map over. Can be an array, async iterable, or promise that resolves to an iterable.
mapper
(item: T, index: number) => Promise<R | SkipSymbol> | R | SkipSymbol
required
The mapping function. Receives the item and its index. Return Skip to omit the item from results.
options
ConcurrencyOptions & ResilienceOptions
Mapping options.

Returns

A promise that resolves with an array of mapped results in the original order.

Throws

  • AbortError if the signal is aborted
  • The first error encountered if stopOnError is true (default)
  • AggregateError containing all errors if stopOnError is false

Examples

Basic mapping

import { map } from '@temelj/async';

const numbers = [1, 2, 3, 4, 5];
const doubled = await map(numbers, async (n) => n * 2);

console.log(doubled); // [2, 4, 6, 8, 10]

Concurrent API requests

import { map } from '@temelj/async';

const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const users = await map(
  userIds,
  async (id) => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  },
  { concurrency: 3 } // Only 3 requests at a time
);

Map with filtering using Skip

import { map, Skip } from '@temelj/async';

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Get only even numbers, doubled
const evenDoubled = await map(numbers, (n) => {
  if (n % 2 === 0) {
    return n * 2;
  }
  return Skip;
});

console.log(evenDoubled); // [4, 8, 12, 16, 20]

Order preservation

import { map } from '@temelj/async';

const items = [3, 1, 2];

// Even though items take different times, order is preserved
const results = await map(items, async (n) => {
  await delay(n * 100); // 300ms, 100ms, 200ms
  return n * 10;
});

console.log(results); // [30, 10, 20] - original order maintained

Error handling with stopOnError

import { map } from '@temelj/async';

const items = [1, 2, 3, 4, 5];

try {
  await map(items, async (n) => {
    if (n === 3) throw new Error('Failed at 3');
    return n * 2;
  }); // stopOnError: true by default
} catch (error) {
  console.error('Stopped on first error:', error.message);
}

Collecting all errors

import { map } from '@temelj/async';

const items = [1, 2, 3, 4, 5];

try {
  await map(
    items,
    async (n) => {
      if (n % 2 === 0) throw new Error(`Failed at ${n}`);
      return n * 2;
    },
    { stopOnError: false }
  );
} catch (error) {
  if (error instanceof AggregateError) {
    console.log('Multiple errors:', error.errors);
    // Returns successful results only
  }
}

With async iterable

import { map } from '@temelj/async';

async function* generateNumbers() {
  for (let i = 1; i <= 5; i++) {
    await delay(100);
    yield i;
  }
}

const results = await map(generateNumbers(), async (n) => n * 2);
console.log(results); // [2, 4, 6, 8, 10]

With promise input

import { map } from '@temelj/async';

const dataPromise = fetchData(); // Returns Promise<number[]>

const results = await map(dataPromise, async (n) => n * 2);

File processing

import { map } from '@temelj/async';
import { readdir, readFile } from 'fs/promises';

const files = await readdir('./data');

const contents = await map(
  files,
  async (filename) => {
    const content = await readFile(`./data/${filename}`, 'utf-8');
    return { filename, content, length: content.length };
  },
  { concurrency: 5 }
);

Image processing pipeline

import { map, Skip } from '@temelj/async';

const imageUrls = [
  'image1.jpg',
  'image2.jpg',
  'invalid.txt',
  'image3.jpg',
];

const processedImages = await map(
  imageUrls,
  async (url) => {
    if (!url.endsWith('.jpg')) {
      return Skip; // Filter out non-images
    }
    
    const image = await loadImage(url);
    const resized = await resizeImage(image, { width: 800 });
    const optimized = await optimizeImage(resized);
    return optimized;
  },
  { concurrency: 2 } // Process 2 images at a time
);

With abort signal

import { map } from '@temelj/async';

const controller = new AbortController();

const task = map(
  [1, 2, 3, 4, 5],
  async (n) => {
    await delay(n * 100);
    return n * 2;
  },
  { signal: controller.signal }
);

// Cancel after 250ms
setTimeout(() => controller.abort(), 250);

try {
  await task;
} catch (error) {
  if (error instanceof AbortError) {
    console.log('Mapping cancelled');
  }
}

Data enrichment

import { map } from '@temelj/async';

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

interface EnrichedUser extends User {
  posts: Post[];
  followers: number;
}

const users: User[] = await getUsers();

const enrichedUsers = await map(
  users,
  async (user): Promise<EnrichedUser> => {
    const [posts, followers] = await Promise.all([
      getUserPosts(user.id),
      getFollowerCount(user.id),
    ]);
    
    return { ...user, posts, followers };
  },
  { concurrency: 5 }
);

Build docs developers (and LLMs) love