Consider three reference points on the spectrum of errors that might occur in a build system:
- The build system broke (we’ll call this “fatal”): the makefile has a bug, a file resource disappeared, a gremlin burrowed through the ram
- A critical build task failed (“error”): the coffeescript has a syntax error, the server couldn’t start
- 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.