Skip to content

Commit ea0f8f9

Browse files
authored
fix: make raiseIfFailed honor failed runtime status (#308)
1 parent 27e363b commit ea0f8f9

2 files changed

Lines changed: 136 additions & 0 deletions

File tree

packages/durabletask-js/src/orchestration/orchestration-state.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,20 @@ export class OrchestrationState {
4848
this.failureDetails,
4949
);
5050
}
51+
52+
// Also check the runtime status for cases where failure details were
53+
// dropped (e.g., empty error message/type from the sidecar).
54+
// Without this check, a FAILED orchestration with missing details would
55+
// silently pass through raiseIfFailed().
56+
if (this.runtimeStatus === OrchestrationStatus.FAILED) {
57+
const syntheticDetails = new FailureDetails(
58+
"Unknown error",
59+
"UnknownError",
60+
);
61+
throw new OrchestrationFailedError(
62+
`Orchestration '${this.instanceId}' failed: ${syntheticDetails.message}`,
63+
syntheticDetails,
64+
);
65+
}
5166
}
5267
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { OrchestrationState, OrchestrationStatus, FailureDetails } from "../src";
5+
import { OrchestrationFailedError } from "../src/orchestration/exception/orchestration-failed-error";
6+
7+
describe("OrchestrationState", () => {
8+
const instanceId = "test-instance-001";
9+
const name = "TestOrchestrator";
10+
const createdAt = new Date("2026-01-01T00:00:00Z");
11+
const lastUpdatedAt = new Date("2026-01-01T00:01:00Z");
12+
13+
function createState(
14+
runtimeStatus: OrchestrationStatus,
15+
failureDetails?: FailureDetails,
16+
): OrchestrationState {
17+
return new OrchestrationState(
18+
instanceId,
19+
name,
20+
runtimeStatus,
21+
createdAt,
22+
lastUpdatedAt,
23+
undefined,
24+
undefined,
25+
undefined,
26+
failureDetails,
27+
);
28+
}
29+
30+
describe("raiseIfFailed", () => {
31+
it("should throw OrchestrationFailedError when failureDetails is present", () => {
32+
const details = new FailureDetails("Something went wrong", "Error", "at foo.ts:1");
33+
const state = createState(OrchestrationStatus.FAILED, details);
34+
35+
expect(() => state.raiseIfFailed()).toThrow(OrchestrationFailedError);
36+
});
37+
38+
it("should include the instance ID and error message when failureDetails is present", () => {
39+
const details = new FailureDetails("Something went wrong", "Error");
40+
const state = createState(OrchestrationStatus.FAILED, details);
41+
42+
try {
43+
state.raiseIfFailed();
44+
throw new Error("Expected raiseIfFailed to throw");
45+
} catch (e: unknown) {
46+
expect(e).toBeInstanceOf(OrchestrationFailedError);
47+
const error = e as OrchestrationFailedError;
48+
expect(error.message).toContain(instanceId);
49+
expect(error.message).toContain("Something went wrong");
50+
expect(error.failureDetails).toBe(details);
51+
}
52+
});
53+
54+
it("should throw when runtimeStatus is FAILED but failureDetails is missing", () => {
55+
const state = createState(OrchestrationStatus.FAILED);
56+
57+
expect(() => state.raiseIfFailed()).toThrow(OrchestrationFailedError);
58+
});
59+
60+
it("should include the instance ID when runtimeStatus is FAILED without details", () => {
61+
const state = createState(OrchestrationStatus.FAILED);
62+
63+
try {
64+
state.raiseIfFailed();
65+
throw new Error("Expected raiseIfFailed to throw");
66+
} catch (e: unknown) {
67+
expect(e).toBeInstanceOf(OrchestrationFailedError);
68+
const error = e as OrchestrationFailedError;
69+
expect(error.message).toContain(instanceId);
70+
expect(error.failureDetails).toBeDefined();
71+
expect(error.failureDetails.errorType).toEqual("UnknownError");
72+
}
73+
});
74+
75+
it("should not throw when runtimeStatus is COMPLETED", () => {
76+
const state = createState(OrchestrationStatus.COMPLETED);
77+
78+
expect(() => state.raiseIfFailed()).not.toThrow();
79+
});
80+
81+
it("should not throw when runtimeStatus is RUNNING", () => {
82+
const state = createState(OrchestrationStatus.RUNNING);
83+
84+
expect(() => state.raiseIfFailed()).not.toThrow();
85+
});
86+
87+
it("should not throw when runtimeStatus is TERMINATED", () => {
88+
const state = createState(OrchestrationStatus.TERMINATED);
89+
90+
expect(() => state.raiseIfFailed()).not.toThrow();
91+
});
92+
93+
it("should not throw when runtimeStatus is PENDING", () => {
94+
const state = createState(OrchestrationStatus.PENDING);
95+
96+
expect(() => state.raiseIfFailed()).not.toThrow();
97+
});
98+
99+
it("should not throw when runtimeStatus is SUSPENDED", () => {
100+
const state = createState(OrchestrationStatus.SUSPENDED);
101+
102+
expect(() => state.raiseIfFailed()).not.toThrow();
103+
});
104+
105+
it("should prioritize failureDetails over synthetic details when both are available", () => {
106+
const details = new FailureDetails("Specific error", "CustomError", "stack trace");
107+
const state = createState(OrchestrationStatus.FAILED, details);
108+
109+
try {
110+
state.raiseIfFailed();
111+
throw new Error("Expected raiseIfFailed to throw");
112+
} catch (e: unknown) {
113+
const error = e as OrchestrationFailedError;
114+
// Should use the real failure details, not synthetic ones
115+
expect(error.failureDetails.message).toEqual("Specific error");
116+
expect(error.failureDetails.errorType).toEqual("CustomError");
117+
expect(error.failureDetails.stackTrace).toEqual("stack trace");
118+
}
119+
});
120+
});
121+
});

0 commit comments

Comments
 (0)