From 8783bbfbbec59acb5616a579b4a87b441c6d6512 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 09:14:02 +0000 Subject: [PATCH] fix: entity executor preserves stack trace in failure details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entity executor's error handling code incorrectly used a chained call pattern (new StringValue().setValue(error.stack)) to set the stack trace on failure details. In google-protobuf, setValue() returns void, not the StringValue instance, so setStacktrace() received undefined — silently discarding the stack trace for all entity operation failures. Fix: construct the StringValue separately and pass it to setStacktrace(), matching the correct pattern used elsewhere in the codebase (pb-helper.util.ts). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/worker/entity-executor.ts | 4 +- .../test/entity-executor.spec.ts | 61 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/durabletask-js/src/worker/entity-executor.ts b/packages/durabletask-js/src/worker/entity-executor.ts index 1061c1a4..54c78c98 100644 --- a/packages/durabletask-js/src/worker/entity-executor.ts +++ b/packages/durabletask-js/src/worker/entity-executor.ts @@ -396,7 +396,9 @@ export class TaskEntityShim { failureDetails.setErrortype(error.name); failureDetails.setErrormessage(error.message); if (error.stack) { - failureDetails.setStacktrace(new StringValue().setValue(error.stack)); + const stackValue = new StringValue(); + stackValue.setValue(error.stack); + failureDetails.setStacktrace(stackValue); } } else { failureDetails.setErrortype("Error"); diff --git a/packages/durabletask-js/test/entity-executor.spec.ts b/packages/durabletask-js/test/entity-executor.spec.ts index aa462d23..3a223e75 100644 --- a/packages/durabletask-js/test/entity-executor.spec.ts +++ b/packages/durabletask-js/test/entity-executor.spec.ts @@ -175,6 +175,67 @@ describe("TaskEntityShim", () => { expect(failure.getFailuredetails()?.getErrormessage()).toBe("Intentional error"); }); + it("should preserve error stack trace in failure details", async () => { + const entity = new CounterEntity(); + const shim = new TaskEntityShim(entity, entityId); + const request = createBatchRequest( + entityId.toString(), + [{ name: "throwError" }], + { count: 0 }, + ); + + const result = await shim.executeAsync(request); + + const opResult = result.getResultsList()[0]; + expect(opResult.hasFailure()).toBe(true); + + const failure = opResult.getFailure()!; + const failureDetails = failure.getFailuredetails()!; + + // Verify error type and message + expect(failureDetails.getErrortype()).toBe("Error"); + expect(failureDetails.getErrormessage()).toBe("Intentional error"); + + // Verify stack trace is properly set as a StringValue (not undefined) + const stackTrace = failureDetails.getStacktrace(); + expect(stackTrace).toBeDefined(); + expect(stackTrace).not.toBeNull(); + expect(stackTrace!.getValue()).toContain("Intentional error"); + expect(stackTrace!.getValue()).toContain("throwError"); + }); + + it("should handle non-Error throws with error type and message", async () => { + // Entity that throws a non-Error value + class StringThrowEntity extends TaskEntity<{ count: number }> { + throwString(): void { + throw "string error"; + } + + protected initializeState(): { count: number } { + return { count: 0 }; + } + } + + const entity = new StringThrowEntity(); + const shim = new TaskEntityShim(entity, entityId); + const request = createBatchRequest( + entityId.toString(), + [{ name: "throwString" }], + { count: 0 }, + ); + + const result = await shim.executeAsync(request); + + const opResult = result.getResultsList()[0]; + expect(opResult.hasFailure()).toBe(true); + + const failureDetails = opResult.getFailure()!.getFailuredetails()!; + expect(failureDetails.getErrortype()).toBe("Error"); + expect(failureDetails.getErrormessage()).toBe("string error"); + // Non-Error throws have no stack trace + expect(failureDetails.getStacktrace()).toBeUndefined(); + }); + it("should continue executing after failed operation", async () => { const entity = new CounterEntity(); const shim = new TaskEntityShim(entity, entityId);