Gulp 4 Task Composition with series() and parallel(): A Practical Guide
Gulp 4 replaces implicit task arrays with explicit series() and parallel() composition, making build pipelines readable, testable, and free of hidden ordering bugs.
17 Sept 2026, 00:20 UTC

The problem: implicit task ordering in Gulp 3
Gulp 3 let you declare dependencies as a string array — gulp.task('build', ['clean', 'scripts']) — but the runner decided the execution order behind the scenes. In a growing codebase this hidden graph caused subtle race conditions: a lint step might start before the transpiler finished, or a clean task could run twice because two parents listed it.
Thesis: explicit composition makes the build graph visible and testable
Gulp 4 replaces the array with two composition functions: series() runs tasks one after another, parallel() runs them concurrently. Both return a new task function that can be nested, exported, or passed to gulp.watch(). The dependency graph becomes ordinary JavaScript, so you can read, refactor, and unit‑test it like any other code.
How series() and parallel() work
- series(a, b, c) – executes
a, waits for its completion signal, thenb, thenc. - parallel(x, y, z) – starts
x,y,ztogether; the composed task finishes when every branch signals completion. - Completion signals: a task must return a stream, promise, event emitter, child process, or call its callback (
cb()). Forgetting a return value makes the CLI hang with “The following tasks did not complete”.
Worked example: a minimal build pipeline
Create gulpfile.js in the project root (requires Node ≥ 12 and npm i -D gulp@4).
const { src, dest, series, parallel, watch } = require('gulp');
const del = require('del');
const sass = require('gulp-sass')(require('sass'));
const babel = require('gulp-babel');
const eslint = require('gulp-eslint');
function clean() { return del(['dist']); }
function styles() { return src('src/**/*.scss').pipe(sass()).pipe(dest('dist/css')); }
function scripts() { return src('src/**/*.js').pipe(babel()).pipe(dest('dist/js')); }
function images() { return src('src/**/*.{png,jpg,svg}').pipe(dest('dist/img')); }
function lint() { return src(['src/**/*.js','gulpfile.js']).pipe(eslint()).pipe(eslint.format()).pipe(eslint.failAfterError()); }
const build = series(clean, parallel(styles, scripts, images), lint);
exports.clean = clean;
exports.styles = styles;
exports.scripts = scripts;
exports.images = images;
exports.lint = lint;
exports.build = build;
exports.default = build;
// Watch with debounced parallel rebuilds
watch('src/**/*.scss', series(styles, lint));
watch('src/**/*.js', series(scripts, lint));
watch('src/**/*.{png,jpg,svg}', images);
Run the pipeline:
# In the project root (no elevated permissions needed)
npx gulp --version # should print 4.x, e.g. 4.0.2
npx gulp build # executes the composed task
Observe the console: clean finishes, then styles, scripts, images start together, and finally lint runs after all three complete.
Trade‑offs and limitations
- No built‑in concurrency limit –
parallel()opens every stream at once. On a project with hundreds of files this can exhaust file descriptors. Mitigate with a plugin likegulp-concurrencyor batch the work in custom tasks. - Error propagation – a rejected promise or stream
erroraborts theserieschain, butparallelbranches continue unless you attach a handler (e.g.,.on('error', cb)orplumber()). - Mixing import styles – using both
require('gulp')andimport { series } from 'gulp'in the same file creates two registry instances, breaking composition. Pick one module system per file. - Backward‑compatibility trap – the old
gulp.task(name, deps, fn)signature still works but cannot be mixed reliably withseries/parallel. Migrate fully to avoid hidden ordering bugs.
How to verify the behavior yourself
- Add a tiny task that logs timestamps:
function stamp(cb) { console.log(new Date().toISOString(), 'stamp'); cb(); } - Compose
series(stamp, parallel(stamp, stamp), stamp)and runnpx gulp. The output should show three distinct time groups, confirming sequential vs concurrent execution. - Remove the
returnfromstyles()and run the build again – the process will hang. Restore the return and the pipeline completes, proving the completion‑signal rule.
Actionable next step
Replace any remaining Gulp 3 dependency arrays with explicit series/parallel compositions. Run the timestamp test to confirm the new order, then add a concurrency limiter if your parallel branch opens more than ~200 simultaneous streams. The result is a build graph you can read, review in code review, and extend without surprise.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.