Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 8 additions & 1 deletion packages/durabletask-js/src/utils/pb-helper.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ export function newSubOrchestrationFailedEvent(eventId: number, ex: Error): pb.H
return event;
}

export function newFailureDetails(e: unknown): pb.TaskFailureDetails {
export function newFailureDetails(e: unknown, _depth: number = 0): pb.TaskFailureDetails {
const MAX_CAUSE_DEPTH = 10;
Comment thread
YunchuWang marked this conversation as resolved.
Outdated
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 +203,12 @@ 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.
if (e instanceof Error && e.cause && _depth < MAX_CAUSE_DEPTH) {
failure.setInnerfailure(newFailureDetails(e.cause, _depth + 1));
}
Comment thread
YunchuWang marked this conversation as resolved.

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 to prevent stack overflow from circular causes", () => {
Comment thread
YunchuWang marked this conversation as resolved.
Outdated
// Build a 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