Direct Answers
Intercepting error propagation: Wrap the failing task in an async function that catches the error, logs it, and returns a resolved promise (or calls the callback without an error argument). This prevents gulp.series() from aborting the chain.
Keeping gulp.watch() alive: Ensure the top-level task passed to watch() never rejects. Either handle errors inside every nested task, or wrap the entire series in a promise that .catch()s and resolves.
Why Errors Propagate
In Gulp 4, series() executes tasks sequentially. If any task signals failure — by rejecting a promise, passing an error to its callback, or emitting an error event on a stream without a listener — the series stops immediately and the error bubbles to the caller. A watch() handler that receives a rejected promise will crash the Node process unless the rejection is handled.
Confirmed Patterns
1. Task-Level Try/Catch (Async Functions)
const { series, task } = require('gulp');
function flakyTask() {
return Promise.resolve()
.then(() => { throw new Error('boom'); })
.catch(err => {
console.error('[flakyTask]', err.message);
// Return a resolved promise so series continues
return Promise.resolve();
});
}
task('build', series(flakyTask, () => console.log('runs after failure')));
The .catch() returns a resolved promise, so series() treats the task as successful and proceeds.
2. Stream Error Listeners That Don't Re-emit
const { src, dest } = require('gulp');
const uglify = require('gulp-uglify');
function scripts() {
return src('src/**/*.js')
.pipe(uglify())
.on('error', err => {
console.error('[uglify]', err.message);
this.emit('end'); // End the stream cleanly
})
.pipe(dest('dist'));
}
Calling this.emit('end') inside the error handler finishes the stream without propagating the error.
3. Wrapper Promise for Watch Tasks
const { series, watch, task } = require('gulp');
function build() {
return series(flakyTask, otherTask)(); // returns a promise
}
task('watch', () => {
watch('src/**/*.js', () => build().catch(() => {}));
});
The .catch(() => {}) on the promise returned by build() swallows any rejection, keeping the watcher running.
Verification Steps
- Create a minimal
gulpfile.js with a failing task inside series().
- Run
gulp build and confirm the process exits with non-zero code (default behavior).
- Apply one of the patterns above, re-run, and verify the process exits with code 0 and subsequent tasks execute.
- Run
gulp watch, trigger a change that causes the failure, and confirm the watcher stays active and logs the error.
Assumptions & Version Notes
- Gulp 4.x (tested on 4.0.2). Behavior differs from Gulp 3.
- Node.js 18+ (standard promise/async support).
- Stream-based tasks use vinyl-fs streams;
this.emit('end') works because Gulp patches the stream instance.
One Diagnostic Detail
Are your failing tasks async functions returning promises, callback-based, or stream-based? The exact wrapper differs slightly for each, and mixing styles in one series can cause silent hangs if completion isn't signaled consistently.