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
4 changes: 3 additions & 1 deletion packages/durabletask-js/src/worker/entity-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
61 changes: 61 additions & 0 deletions packages/durabletask-js/test/entity-executor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading