Mastering Gulp 4 Task Composition with series() and parallel()
Gulp 4 replaces the old dependency-based task system with explicit series() and parallel() functions, giving developers full control over execution order and concurrency.
07 Apr 2026, 08:31 UTC

The problem: unpredictable build order
When a gulpfile grows beyond a handful of tasks, the old dependency-array syntax gulp.task('build', ['clean', 'compile']) becomes fragile. Gulp 4 removed this implicit ordering, and without explicit composition tasks can start in unexpected sequences or hang because completion signals are unclear.
Thesis: explicit composition with series() and parallel()
Gulp 4 provides two composable functions that replace the dependency system. series() guarantees the first task finishes before the second starts. parallel() starts both concurrently and completes when both report done. Each task must return a stream, promise, or callback to signal finish.
How series() works
series() waits for every argument to resolve before proceeding. If a task returns a stream (as is common with gulp plugins) or a promise, Gulp pipes the resolution through automatically. Mixing synchronous file operations, promises, and streams in the same series chain is supported as long as each step surfaces an async completion token.
How parallel() works
parallel() starts all listed tasks nearly at the same time. Gulp considers the combined task complete only when every branch has signaled finish. This is safe only when branches operate on independent files or directories; sharing output paths can cause race conditions.
Worked example: a web-asset pipeline
const { src, dest, series, parallel, watch } = require('gulp');
const del = require('del');
const sass = require('gulp-sass')(require('sass'));
const concat = require('gulp-concat');
function clean() {
return del(['dist']);
}
function styles() {
return src('src/scss/**/*.scss')
.pipe(sass().on('error', sass.logError))
.pipe(dest('dist/css'));
}
function scripts() {
return src('src/js/**/*.js')
.pipe(concat('bundle.js'))
.pipe(dest('dist/js'));
}
function watchFiles() {
watch('src/scss/**/*.scss', styles);
watch('src/js/**/*.js', scripts);
}
exports.clean = clean;
exports.styles = styles;
exports.scripts = scripts;
exports.watch = watchFiles;
// default task: clean -> [styles, scripts] -> watch
exports.default = series(clean, parallel(styles, scripts), watchFiles);
To run this locally:
- Ensure Node.js
≥12and npm are installed. - Run
npm init -yif nopackage.jsonexists. - Install dependencies:
npm install --save-dev gulp gulp-sass sass del gulp-concat. - Save the snippet as
gulpfile.jsin your project root. - Execute the default task:
npx gulp. You should seecleanrun first, thenstylesandscriptsstart concurrently, followed by the watch prompt.
To verify ordering, add a brief console.log at the start of each task and run npx gulp. Observe that the clean log appears before the style and script logs, and that style and script logs may overlap, indicating concurrent execution. The npx gulp --trace flag prints the internal task tree, showing series and parallel nodes as expected.
Trade-off and limitation
The primary risk with parallel() is a race condition when two branches write to the same file or folder. Interleaved or corrupted output can result. Always direct each parallel branch to a distinct destination (e.g., separate subfolders) or use a file-locking plugin. Additionally, if a task does not return a stream, promise, or call its callback, Gulp will wait indefinitely and the build appears to hang.
Actionable closing
Migrate legacy gulp.task('name', ['dep'], fn) definitions to plain functions and compose them with series() and parallel(). Keep tasks focused, ensure each returns a clear async signal, and validate the flow with --trace before integrating into CI. This yields a readable, maintainable gulpfile that behaves exactly as declared.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.