Error Handling in gulp

Photo of inside fo tackle box by Harrison Kugler on Unsplash

Consider three reference points on the spectrum of errors that might occur in a build system:

  1. The build system broke (we’ll call this “fatal”): the makefile has a bug, a file resource disappeared, a gremlin burrowed through the ram
  2. A critical build task failed (“error”): the coffeescript has a syntax error, the server couldn’t start
  3. A validation task failed (“warning): the sass has a lint warning, a behavior test failed

Should the build fail for each of these error types?  That depends on the context:

  • In a CI server, everything down to a validation warning should result in a failed build
  • In a watch / live-reload context, errors and warnings should be logged but the build should keep (continually) running
  • In a one-off local development build, the “fatal” and “error” levels should cancel the build but a “warning” should not (well, a TDD developer would disagree)

How can these error permutations be handled in a gulp build?

Error handling in gulpfile.js

For typical gulp plugin usage, any errors that occur will be emitted on the stream and can be easily caught and logged:

gulp.task('coffee', function() {
   gulp.src(testfiles)
      .pipe(coffee())
      .on('error', gutil.log);
}

That’s only the beginning of the story.  To handle the different build contexts described above we’ll write an error handler (to replace gutil.log in the above code) that will log errors and halt the build process when appropriate.

Determine the build context

Since the same tasks can be run in different contexts (CI server, local dev, watch) we’ll use a flag that sets the fatality error level:

var fatalLevel = require('yargs').argv.fatal;

This flag can be set through a gulp command line option:

$ gulp                  # defaults to fatal=error as we'll see in a moment
$ gulp --fatal=error
$ gulp --fatal=warning
$ gulp --fatal=off      # no errors should kill the build

Determine if the error is fatal

Assuming each error is tagged with a level of error or warning, then we can write a function to determine whether an error is fatal or not based on the command line fatality flag:

var ERROR_LEVELS = ['error', 'warning'];

function isFatal(level) {
   return ERROR_LEVELS.indexOf(level) <= ERROR_LEVELS.indexOf(fatalLevel || 'error');
}

isFatal() returns true if the given level is equal to or more severe than the configured fatality error level. If the fatality level is off this always returns true. It defaults the fatality level to error if it has not yet been set by the user or some task default.

Error handler

function handleError(level, error) {
   gutil.log(error.message);
   if (isFatal(level)) {
      process.exit(1);
   }
}

function onError(error) { handleError.call(this, 'error', error);}
function onWarning(error) { handleError.call(this, 'warning', error);}

handleError() simply logs the error message and kills the process (with an error exit status) if the error level matches or exceeds the fatality level. onError and onWarning are helpers that can be passed directly as callbacks to the on('error', callback) stream method.

Insert the error handler into task streams

Handling errors and warnings in task streams is now easy.  Our CoffeeScript lint task will handle lint errors as warnings, and our CoffeeScript compile task will handle compilation errors as errors.

gulp.task('coffeelint', function() {
   gulp.src(testfiles)
      .pipe(coffeelint())
      .on('error', onWarning);
}

gulp.task('coffee', function() {
   gulp.src(testfiles)
      .pipe(coffee())
      .on('error', onError);
}

We can default the fatality level to off for the watch task so that it never fails, but still logs warnings and errors:

gulp.task('watch', function() {
   fatalLevel = fatalLevel || 'off';
   gulp.watch(testfiles, ['coffeelint', 'coffee']);
});

Example gulpfile

Here’s an example gulpfile in its entirety:

var gulp = require('gulp');
var gutil = require('gulp-util');
var jshint = require('gulp-jshint');

// Command line option:
//  --fatal=[warning|error|off]
var fatalLevel = require('yargs').argv.fatal;

var ERROR_LEVELS = ['error', 'warning'];

// Return true if the given level is equal to or more severe than
// the configured fatality error level.
// If the fatalLevel is 'off', then this will always return false.
// Defaults the fatalLevel to 'error'.
function isFatal(level) {
   return ERROR_LEVELS.indexOf(level) <= ERROR_LEVELS.indexOf(fatalLevel || 'error');
}

// Handle an error based on its severity level.
// Log all levels, and exit the process for fatal levels.
function handleError(level, error) {
   gutil.log(error.message);
   if (isFatal(level)) {
      process.exit(1);
   }
}

// Convenience handler for error-level errors.
function onError(error) { handleError.call(this, 'error', error);}
// Convenience handler for warning-level errors.
function onWarning(error) { handleError.call(this, 'warning', error);}

var testfiles = ['error.js', 'warning.js'];

// Task that emits an error that's treated as a warning.
gulp.task('warning', function() {
   gulp.src(testfiles).
      pipe(jshint()).
      pipe(jshint.reporter('fail')).
      on('error', onWarning);
});

// Task that emits an error that's treated as an error.
gulp.task('error', function() {
   gulp.src(testfiles).
      pipe(jshint()).
      pipe(jshint.reporter('fail')).
      on('error', onError);
});

gulp.task('watch', function() {
   // By default, errors during watch should not be fatal.
   fatalLevel = fatalLevel || 'off';
   gulp.watch(testfiles, ['error']);
});

gulp.task('default', ['watch']);

What else?

There’s more potential complexity to error handling in gulp than I’ve covered.

For example, gulp-plumber helps to prevent stream errors from breaking streams (though it may be unnecessary since gulp’s watch method was recently patched to better handle errors).

There have been lengthy discussions about how plugins should report errors, such as whether gulp-jshint should fail a build.  The questions of when and how to return an error seem unsettled in the plugin author community, which makes sense since expectations might be quite different per user, project, and context.

There’s also a plan (discussed in controlling failing builds) to support deferring a build failure until all tasks have run so that a jshint error causes a failure but still allows other tasks to complete.

It looks like the next few months, and gulp 4 (anyone know the status?), will bring progressive change to error handling and hopefully further solidify best practices across the ecosystem.

Noah Harrison

Noah Harrison

Senior software engineer at Art+Logic.
Noah Harrison

Latest posts by Noah Harrison (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

1 Comment

  1. Robert Balicki

    Hi Noah, I published an npm package, pipe-error-stop (see npmjs.org/package/pipe-error-stop), that makes the process of handling errors easier and more graceful. Essentially, you wrap a stream in pipeErrorStop, and if the stream emits an error, no data will be emitted. Otherwise, it is all flushed at once. This is useful, for example, with gulp-ruby-sass, which emits errors but also emits data. pipeErrorStop would prevent sass errors from showing up on your homepage when you add an extra semicolon.

    Hope that helps. -R