Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/durabletask-js/src/utils/pb-helper.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ export function newSubOrchestrationFailedEvent(eventId: number, ex: Error): pb.H
}

export function newFailureDetails(e: unknown): pb.TaskFailureDetails {
return buildFailureDetails(e, 0);
}

/**
* Recursively builds TaskFailureDetails, populating innerFailure from error.cause.
* The depth parameter is internal to bound the cause-chain recursion and is not
* exposed on the public newFailureDetails() signature.
*/
function buildFailureDetails(e: unknown, depth: number): pb.TaskFailureDetails {
const MAX_CAUSE_DEPTH = 10;
const failure = new pb.TaskFailureDetails();
// Use e.name (which can be customized) over constructor.name (which is always the class name)
// This allows users to set error.name = "CustomError" and have it preserved in failure details
Expand All @@ -202,6 +212,14 @@ export function newFailureDetails(e: unknown): pb.TaskFailureDetails {
failure.setStacktrace(sv);
}

// Populate innerFailure from error.cause to preserve the full error chain.
// A depth limit guards against pathological circular cause chains.
// error.cause can be any value, so guard against null/undefined explicitly rather
// than truthiness — a falsy-but-present cause (e.g. "" or 0) is still a valid cause.
if (e instanceof Error && e.cause != null && depth < MAX_CAUSE_DEPTH) {
failure.setInnerfailure(buildFailureDetails(e.cause, depth + 1));
}

return failure;
}

Expand Down
80 changes: 80 additions & 0 deletions packages/durabletask-js/test/pb-helper-versioning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,85 @@ describe("pb-helper.util - Version Mismatch Failure Details", () => {
expect(failure.getErrortype()).toBe("CustomError");
expect(failure.getErrormessage()).toBe("Custom error message");
});

it("should populate innerFailure from error.cause", () => {
const innerError = new TypeError("Cannot read property 'x' of undefined");
const outerError = new Error("Failed to process input", { cause: innerError });
const failure = newFailureDetails(outerError);

expect(failure.getErrortype()).toBe("Error");
expect(failure.getErrormessage()).toBe("Failed to process input");

const inner = failure.getInnerfailure();
expect(inner).toBeDefined();
expect(inner!.getErrortype()).toBe("TypeError");
expect(inner!.getErrormessage()).toBe("Cannot read property 'x' of undefined");
expect(inner!.getStacktrace()).toBeDefined();
});

it("should handle multi-level error cause chains", () => {
const rootCause = new RangeError("Index out of bounds");
const midError = new Error("Data access failed", { cause: rootCause });
const topError = new Error("Operation failed", { cause: midError });
const failure = newFailureDetails(topError);

expect(failure.getErrortype()).toBe("Error");
expect(failure.getErrormessage()).toBe("Operation failed");

const mid = failure.getInnerfailure();
expect(mid).toBeDefined();
expect(mid!.getErrortype()).toBe("Error");
expect(mid!.getErrormessage()).toBe("Data access failed");

const root = mid!.getInnerfailure();
expect(root).toBeDefined();
expect(root!.getErrortype()).toBe("RangeError");
expect(root!.getErrormessage()).toBe("Index out of bounds");
expect(root!.getInnerfailure()).toBeUndefined();
});

it("should handle non-Error cause values", () => {
const outerError = new Error("Wrapper", { cause: "string cause" });
const failure = newFailureDetails(outerError);

const inner = failure.getInnerfailure();
expect(inner).toBeDefined();
expect(inner!.getErrortype()).toBe("UnknownError");
expect(inner!.getErrormessage()).toBe("string cause");
});

it("should not set innerFailure when there is no cause", () => {
const error = new Error("No cause");
const failure = newFailureDetails(error);

expect(failure.getInnerfailure()).toBeUndefined();
});

it("should not set innerFailure for non-Error values", () => {
const failure = newFailureDetails("plain string error");

expect(failure.getErrortype()).toBe("UnknownError");
expect(failure.getErrormessage()).toBe("plain string error");
expect(failure.getInnerfailure()).toBeUndefined();
});

it("should cap recursion depth for deep cause chains", () => {
// Build a linear cause chain deeper than the limit (10)
let error: Error = new Error("root");
for (let i = 0; i < 15; i++) {
error = new Error(`level-${i}`, { cause: error });
}

const failure = newFailureDetails(error);

// Walk down the chain — should stop at depth 10
let current = failure;
let depth = 0;
while (current.getInnerfailure()) {
current = current.getInnerfailure()!;
depth++;
}
expect(depth).toBe(10);
});
});
});
Loading