Comprehensive error system for build failures, node setup issues, and cancellation. Broccoli provides a hierarchy of error classes for different failure scenarios.
Base error class for all builder-related errors.
/**
* Base error class for builder errors
*/
class BuilderError extends Error {
/** Identifies this as a builder error */
readonly isBuilderError: boolean;
/**
* Creates a new BuilderError
* @param message - Error message (optional)
*/
constructor(message?: string);
/**
* Check if an error is a BuilderError
* @param error - Error to check
* @returns true if error is a BuilderError
*/
static isBuilderError(error: any): boolean;
}Usage Examples:
const { BuilderError } = require("broccoli");
try {
await builder.build();
} catch (error) {
if (BuilderError.isBuilderError(error)) {
console.log('Builder error occurred:', error.message);
console.log('Is builder error:', error.isBuilderError);
} else {
console.log('Non-builder error:', error);
}
}
// Creating custom builder errors
class CustomBuilderError extends BuilderError {
constructor(details: string) {
super(`Custom error: ${details}`);
}
}
throw new CustomBuilderError('Something went wrong');Error thrown during the build process, containing detailed build failure information.
/**
* Error thrown during build process with detailed failure information
*/
class BuildError extends BuilderError {
/** Whether this error should be displayed silently */
readonly isSilent: boolean;
/** Whether this error represents a build cancellation */
readonly isCancellation: boolean;
/** Detailed error payload with build context */
readonly broccoliPayload: BroccoliPayloadError;
/**
* Creates a new BuildError
* @param originalError - The original error that caused the build failure
* @param nodeWrapper - Optional node wrapper where the error occurred
*/
constructor(originalError: any, nodeWrapper?: NodeWrapper);
}
interface BroccoliPayloadError {
/** Location information for the error */
location?: {
file?: string;
line?: number;
column?: number;
};
/** Build node where error occurred */
node?: NodeWrapper;
/** Stack trace information */
stack?: string;
/** Additional error details */
details?: any;
}Usage Examples:
const { BuildError } = require("broccoli");
try {
await builder.build();
} catch (error) {
if (error instanceof BuildError) {
console.log('Build failed');
console.log('Silent error:', error.isSilent);
console.log('Cancellation:', error.isCancellation);
if (error.broccoliPayload) {
const payload = error.broccoliPayload;
if (payload.location) {
console.log(`Error at ${payload.location.file}:${payload.location.line}:${payload.location.column}`);
}
if (payload.node) {
console.log(`Error in node: ${payload.node.label}`);
}
if (payload.details) {
console.log('Error details:', payload.details);
}
}
}
}
// Creating build errors
const nodeWrapper = builder.nodeWrappers[0];
const originalError = new Error('Transform failed');
const buildError = new BuildError(originalError, nodeWrapper);Error thrown during node setup phase.
/**
* Error thrown during node setup phase
*/
class NodeSetupError extends BuilderError {
/**
* Creates a new NodeSetupError
* @param originalError - The original error that caused setup failure
* @param nodeWrapper - Optional node wrapper being set up
*/
constructor(originalError: Error, nodeWrapper?: NodeWrapper);
}Usage Examples:
const { NodeSetupError } = require("broccoli");
try {
builder.makeNodeWrapper(node);
} catch (error) {
if (error instanceof NodeSetupError) {
console.log('Node setup failed:', error.message);
// Access the original error
console.log('Original error:', error.cause);
// Handle setup failures
console.log('Failed to set up build node, check plugin configuration');
}
}CLI-specific errors for command line interface failures.
/**
* CLI-specific errors
*/
class CliError extends Error {
/**
* Creates a new CliError
* @param message - Error message
*/
constructor(message: string);
}Usage Examples:
const { CliError, cli } = require("broccoli");
try {
await cli(['build', 'dist', '--invalid-flag']);
} catch (error) {
if (error instanceof CliError) {
console.error('CLI Error:', error.message);
process.exit(1);
}
}
// Creating CLI errors
function validateArgs(args: string[]): void {
if (args.length === 0) {
throw new CliError('No command specified');
}
const validCommands = ['build', 'serve'];
if (!validCommands.includes(args[0])) {
throw new CliError(`Invalid command: ${args[0]}. Valid commands: ${validCommands.join(', ')}`);
}
}Error class for build cancellation scenarios.
/**
* Cancellation error for build interruption
*/
class Cancelation extends Error {
/** Identifies this as a cancellation error */
readonly isCancelation: boolean;
/** Whether this cancellation should be displayed silently */
readonly isSilent: boolean;
/**
* Creates a new Cancelation error
* @param message - Cancellation message (optional)
*/
constructor(message?: string);
/**
* Check if an error is a cancellation error
* @param error - Error to check
* @returns true if error represents cancellation
*/
static isCancelationError(error: any): boolean;
}Usage Examples:
const { Cancelation } = require("broccoli");
// Handle cancellation
try {
await builder.build();
} catch (error) {
if (Cancelation.isCancelationError(error)) {
console.log('Build was cancelled');
console.log('Silent cancellation:', error.isSilent);
// Don't treat cancellation as a failure
process.exit(0);
}
}
// Manual cancellation
async function buildWithTimeout(builder: Builder, timeoutMs: number): Promise<void> {
const buildPromise = builder.build();
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Cancelation('Build timed out'));
}, timeoutMs);
});
try {
await Promise.race([buildPromise, timeoutPromise]);
} catch (error) {
if (error instanceof Cancelation) {
await builder.cancel();
}
throw error;
}
}Common patterns for handling different types of errors.
Comprehensive Error Handling:
const {
BuilderError,
BuildError,
NodeSetupError,
CliError,
Cancelation
} = require("broccoli");
async function safeBuild(builder: Builder): Promise<boolean> {
try {
await builder.build();
console.log('Build successful');
return true;
} catch (error) {
// Handle cancellation
if (Cancelation.isCancelationError(error)) {
console.log('Build cancelled');
return false;
}
// Handle build errors
if (error instanceof BuildError) {
console.error('Build failed:', error.message);
if (!error.isSilent) {
console.error('Build error details:', error.broccoliPayload);
}
return false;
}
// Handle node setup errors
if (error instanceof NodeSetupError) {
console.error('Node setup failed:', error.message);
console.error('Check your Brocfile and plugin configuration');
return false;
}
// Handle CLI errors
if (error instanceof CliError) {
console.error('CLI error:', error.message);
return false;
}
// Handle general builder errors
if (BuilderError.isBuilderError(error)) {
console.error('Builder error:', error.message);
return false;
}
// Handle unexpected errors
console.error('Unexpected error:', error);
throw error;
}
}Retry Logic with Error Handling:
async function buildWithRetry(
builder: Builder,
maxRetries: number = 3
): Promise<void> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await builder.build();
return; // Success
} catch (error) {
lastError = error;
// Don't retry cancellations
if (Cancelation.isCancelationError(error)) {
throw error;
}
// Don't retry CLI errors
if (error instanceof CliError) {
throw error;
}
// Don't retry node setup errors
if (error instanceof NodeSetupError) {
throw error;
}
// Retry build errors
if (error instanceof BuildError && attempt < maxRetries) {
console.log(`Build attempt ${attempt} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
continue;
}
throw error;
}
}
throw lastError;
}Error Reporting and Logging:
const { writeFileSync } from 'fs';
const { join } from 'path';
class ErrorReporter {
private errorLog: any[] = [];
logError(error: Error, context?: any): void {
const errorEntry = {
timestamp: new Date().toISOString(),
message: error.message,
stack: error.stack,
type: error.constructor.name,
context
};
// Add specific error details
if (error instanceof BuildError) {
errorEntry.context = {
...errorEntry.context,
isSilent: error.isSilent,
isCancellation: error.isCancellation,
payload: error.broccoliPayload
};
}
if (error instanceof Cancelation) {
errorEntry.context = {
...errorEntry.context,
isCancelation: error.isCancelation,
isSilent: error.isSilent
};
}
this.errorLog.push(errorEntry);
// Write to file
this.writeErrorLog();
}
private writeErrorLog(): void {
const logPath = join(process.cwd(), 'broccoli-errors.log');
writeFileSync(logPath, JSON.stringify(this.errorLog, null, 2));
}
}
// Usage
const errorReporter = new ErrorReporter();
try {
await builder.build();
} catch (error) {
errorReporter.logError(error, {
buildId: builder.buildId,
outputPath: builder.outputPath,
nodeCount: builder.nodeWrappers.length
});
throw error;
}Graceful Error Recovery:
class ResilientBuilder {
private builder: Builder;
private fallbackBuilder?: Builder;
constructor(builder: Builder, fallbackBuilder?: Builder) {
this.builder = builder;
this.fallbackBuilder = fallbackBuilder;
}
async build(): Promise<string> {
try {
await this.builder.build();
return this.builder.outputPath;
} catch (error) {
console.warn('Primary build failed:', error.message);
// Try fallback builder for certain errors
if (this.fallbackBuilder && this.shouldUseFallback(error)) {
console.log('Attempting fallback build...');
try {
await this.fallbackBuilder.build();
return this.fallbackBuilder.outputPath;
} catch (fallbackError) {
console.error('Fallback build also failed:', fallbackError.message);
throw error; // Throw original error
}
}
throw error;
}
}
private shouldUseFallback(error: Error): boolean {
// Use fallback for build errors but not setup errors
return error instanceof BuildError && !error.isCancellation;
}
}