p-waterfall vs p-all vs p-series vs p-props
Managing Concurrent and Sequential Promise Workflows in JavaScript
p-waterfallp-allp-seriesp-propsSimilar Packages:
Managing Concurrent and Sequential Promise Workflows in JavaScript

p-all, p-props, p-series, and p-waterfall are utility packages from the popular p-* family designed to handle common asynchronous control flow patterns in JavaScript. They provide clean, composable abstractions over native Promise APIs for scenarios where you need to run multiple async operations with specific execution semantics — such as running them all at once, preserving object structure, executing one after another, or passing results between steps. These tools help avoid deeply nested .then() chains or complex manual orchestration while maintaining readability and error handling consistency.

Npm Package Weekly Downloads Trend
3 Years
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
p-waterfall1,878,48878-05 years agoMIT
p-all1,473,7983445.6 kB02 months agoMIT
p-series51,05471-04 years agoMIT
p-props41,33719911 kB02 months agoMIT

Managing Async Workflows: p-all vs p-props vs p-series vs p-waterfall

When building real-world JavaScript applications, you often need more than just Promise.all() or basic chaining. The p-* family of utilities (p-all, p-props, p-series, p-waterfall) fills this gap by offering focused, readable solutions for common async orchestration patterns. Let’s break down how they differ and when to reach for each.

🔄 Execution Model: Parallel vs Sequential vs Chained

p-all runs all promises in parallel, like Promise.all(), but gives you fine-grained control over failure behavior.

import pAll from 'p-all';

const urls = ['/api/users', '/api/posts', '/api/comments'];
const fetchers = urls.map(url => () => fetch(url));

// Runs all fetches concurrently
const responses = await pAll(fetchers, { stopOnError: false });
// Returns array of results; failed requests become `undefined`

p-props also runs promises in parallel, but works with objects instead of arrays, preserving key names.

import pProps from 'p-props';

const data = {
  user: fetch('/api/user'),
  settings: fetch('/api/settings'),
  notifications: () => fetch('/api/notifications') // supports functions too
};

const resolved = await pProps(data);
// Result: { user: ..., settings: ..., notifications: ... }

p-series executes tasks one at a time, in sequence, without passing data between them.

import pSeries from 'p-series';

const tasks = [
  () => sendEmail('welcome'),
  () => logActivity('user_onboarded'),
  () => updateAnalytics()
];

await pSeries(tasks);
// Each runs only after the prior finishes, but no data flows between them

p-waterfall runs tasks sequentially, but passes the result of each step to the next.

import pWaterfall from 'p-waterfall';

const steps = [
  () => getUserById(123),
  (user) => fetchProfile(user.profileId),
  (profile) => enrichWithPreferences(profile)
];

const finalResult = await pWaterfall(steps);
// Output of step 1 → input of step 2 → input of step 3

📦 Input and Output Shape Matters

The shape of your data heavily influences which tool fits best.

  • Use p-all when your inputs are positional (an array) and you care about index-based ordering of results.
  • Use p-props when your inputs are semantic (an object) and you want to keep named properties in the output.
  • Use p-series when you have a list of independent actions that must happen in order (e.g., cleanup steps).
  • Use p-waterfall when you have a pipeline of transformations where each stage builds on the last.

💡 Tip: If you try to use p-series for data transformation, you’ll end up manually managing intermediate variables. That’s a sign you actually need p-waterfall.

⚠️ Error Handling Behavior

All four packages propagate rejections by default — if any promise rejects, the whole chain fails fast. However, p-all and p-props support { stopOnError: false }, which lets you collect partial results:

// With p-all
const results = await pAll([good(), bad(), good()], { stopOnError: false });
// [value, undefined, value]

// With p-props
const data = await pProps({ a: good(), b: bad() }, { stopOnError: false });
// { a: value, b: undefined }

In contrast, p-series and p-waterfall always stop on the first error. There’s no built-in way to skip failed steps because their sequential nature assumes dependency or intentional ordering. If you need resilience in a series, wrap individual steps in try/catch or use p-all with a single-element array per step (though that defeats the purpose).

🛠️ Real-World Scenarios

Scenario 1: Dashboard Data Loading

You’re loading a dashboard with user info, recent orders, and alerts — all independent.

  • Best choice: p-props
  • Why? Keys like userInfo, orders, alerts make the code self-documenting.
const dashboardData = await pProps({
  userInfo: api.getUser(),
  orders: api.getRecentOrders(),
  alerts: api.getUnreadAlerts()
});

Scenario 2: Multi-Step Form Submission

After form submit, you must: validate → save to DB → send confirmation email → log event.

  • Best choice: p-series
  • Why? Steps are side effects with no data passed between them, but order matters.
await pSeries([
  () => validate(formData),
  () => db.save(formData),
  () => email.sendConfirmation(),
  () => logger.log('form_submitted')
]);

Scenario 3: Data Transformation Pipeline

You receive raw CSV → parse → validate rows → enrich with external API → store.

  • Best choice: p-waterfall
  • Why? Each step consumes and modifies the output of the prior.
const result = await pWaterfall([
  () => fs.readFile('data.csv'),
  (buffer) => csv.parse(buffer),
  (rows) => validator.clean(rows),
  (cleanRows) => enricher.addGeoData(cleanRows)
]);

Scenario 4: Bulk Image Uploads

Upload 50 images concurrently, but continue even if some fail.

  • Best choice: p-all with stopOnError: false
  • Why? You want maximum concurrency and tolerance for partial failure.
const uploaders = imageFiles.map(file => () => upload(file));
const results = await pAll(uploaders, { stopOnError: false });
const successful = results.filter(Boolean);

🔄 When Not to Use These

These utilities shine in medium-complexity async flows, but consider alternatives when:

  • You need true concurrency control (e.g., max 5 simultaneous requests): use p-limit instead.
  • You’re working with observables or streams: RxJS or async/await with generators may be better.
  • Your logic is simple enough for native Promise.all() or basic chaining — don’t add deps unnecessarily.

🔁 Summary Table

PackageExecutionInput TypeOutput ShapePasses Data?Tolerant Errors?
p-allParallelArrayArray (ordered)✅ (stopOnError: false)
p-propsParallelObjectObject (same keys)✅ (stopOnError: false)
p-seriesSequentialArrayLast return value
p-waterfallSequentialArrayFinal return value

💡 Final Recommendation

Think in terms of data flow and dependency:

  • No dependencies between tasks? → Go parallel with p-all (array) or p-props (object).
  • Tasks depend on previous output? → Use p-waterfall.
  • Tasks must run in order but don’t share data? → Use p-series.

These small, single-purpose utilities reduce boilerplate and make async intent obvious — a win for maintainability in any serious JavaScript codebase.

How to Choose: p-waterfall vs p-all vs p-series vs p-props
  • p-waterfall:

    Choose p-waterfall when each async step depends on the result of the previous one, forming a linear data pipeline. This pattern shines in ETL workflows, multi-stage validation, or chained transformations where output from one function becomes input to the next.

  • p-all:

    Choose p-all when you need to run an array of promises concurrently and collect their resolved values in order, similar to Promise.all() but with better error handling options (e.g., ignoring failures). It’s ideal for batch operations like fetching multiple independent API endpoints where partial success is acceptable.

  • p-series:

    Choose p-series when tasks must execute strictly one after another, and each step does not depend on the previous result. It’s perfect for ordered side effects like sequential database migrations, logging pipelines, or rate-limited API calls where concurrency would violate constraints.

  • p-props:

    Choose p-props when your input is an object whose values are promises or functions returning promises, and you want the output to preserve the same keys with resolved values. This is especially useful for config loading, feature flag resolution, or hydrating UI state from multiple async sources while keeping semantic key names.

README for p-waterfall

p-waterfall

Run promise-returning & async functions in series, each passing its result to the next

Install

$ npm install p-waterfall

Usage

import pWaterfall from 'p-waterfall';

const tasks = [
	initialValue => getEmoji(initialValue),
	previousValue => `I ❤️ ${previousValue}`
];

console.log(await pWaterfall(tasks, 'unicorn'));
//=> 'I ❤️ 🦄'

API

pWaterfall(tasks, initialValue?)

Returns a Promise that is fulfilled when all promises returned from calling the functions in tasks are fulfilled, or rejects if any of the promises reject. The fulfilled value is the value returned from the last task.

tasks

Type: Iterable<Function>

Functions are expected to return a value. If a Promise is returned, it's awaited before continuing with the next task.

initialValue

Type: unknown

Value to use as previousValue in the first task.

Related


Get professional support for this package with a Tidelift subscription
Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies.