feat(error/error-hierarchy): implement typed error class hierarchy

- Add typed fields to all error subclasses (taskId, cycles, field, message, prerequisite, dependent)
- Set Object.setPrototypeOf(this, new.target.prototype) in all constructors
- Add InvalidInputError.fromTypeBoxError() static factory for TypeBox Value.Errors() output
- CircularDependencyError accepts string[][] for cycle paths
- 31 unit tests covering instanceof chain, field access, .name property, and error messages
This commit is contained in:
2026-04-27 10:00:40 +00:00
parent bd8a7b06d0
commit ce68271f4f
3 changed files with 316 additions and 17 deletions

View File

@@ -1,44 +1,87 @@
// Error classes — TaskgraphError, TaskNotFoundError, CircularDependencyError,
// InvalidInputError, DuplicateNodeError, DuplicateEdgeError
//
// All errors extend TaskgraphError (which extends Error) and set this.name
// to their class name. Object.setPrototypeOf is called in each constructor
// to ensure instanceof works correctly across the prototype chain when
// transpiled to ES5 targets or when extending built-ins.
export class TaskgraphError extends Error {
constructor(message: string) {
super(message);
this.name = 'TaskgraphError';
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class TaskNotFoundError extends TaskgraphError {
readonly taskId: string;
constructor(taskId: string) {
super(`Task not found: ${taskId}`);
this.name = 'TaskNotFoundError';
Object.setPrototypeOf(this, new.target.prototype);
this.taskId = taskId;
}
}
export class CircularDependencyError extends TaskgraphError {
constructor(cycle: string[]) {
super(`Circular dependency detected: ${cycle.join(' → ')}`);
readonly cycles: string[][];
constructor(cycles: string[][]) {
const cycleDescriptions = cycles
.map((cycle) => cycle.join(' → '))
.join('; ');
super(`Circular dependency detected: ${cycleDescriptions}`);
this.name = 'CircularDependencyError';
Object.setPrototypeOf(this, new.target.prototype);
this.cycles = cycles;
}
}
export class InvalidInputError extends TaskgraphError {
constructor(message: string) {
super(message);
readonly field: string;
override readonly message: string;
constructor(field: string, message: string) {
super(`Invalid input for field "${field}": ${message}`);
this.name = 'InvalidInputError';
Object.setPrototypeOf(this, new.target.prototype);
this.field = field;
this.message = message;
}
/**
* Create an InvalidInputError from a TypeBox Value.Errors() iterator entry.
* TypeBox error entries have `path` (e.g. "/dependsOn") and `message` fields.
* The path is stripped of the leading "/" to produce the field name.
*/
static fromTypeBoxError(error: { path: string; message: string }): InvalidInputError {
const field = error.path.startsWith('/') ? error.path.slice(1) : error.path;
return new InvalidInputError(field, error.message);
}
}
export class DuplicateNodeError extends TaskgraphError {
constructor(nodeId: string) {
super(`Duplicate node: ${nodeId}`);
readonly taskId: string;
constructor(taskId: string) {
super(`Duplicate node: ${taskId}`);
this.name = 'DuplicateNodeError';
Object.setPrototypeOf(this, new.target.prototype);
this.taskId = taskId;
}
}
export class DuplicateEdgeError extends TaskgraphError {
constructor(source: string, target: string) {
super(`Duplicate edge: ${source}${target}`);
readonly prerequisite: string;
readonly dependent: string;
constructor(prerequisite: string, dependent: string) {
super(`Duplicate edge: ${prerequisite}${dependent}`);
this.name = 'DuplicateEdgeError';
Object.setPrototypeOf(this, new.target.prototype);
this.prerequisite = prerequisite;
this.dependent = dependent;
}
}