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
15 changes: 15 additions & 0 deletions packages/durabletask-js/src/orchestration/orchestration-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,20 @@ export class OrchestrationState {
this.failureDetails,
);
}

// Also check the runtime status for cases where failure details were
// dropped (e.g., empty error message/type from the sidecar).
// Without this check, a FAILED orchestration with missing details would
// silently pass through raiseIfFailed().
if (this.runtimeStatus === OrchestrationStatus.FAILED) {
const syntheticDetails = new FailureDetails(
"Unknown error",
"UnknownError",
);
throw new OrchestrationFailedError(
`Orchestration '${this.instanceId}' failed: ${syntheticDetails.message}`,
syntheticDetails,
);
}
}
}
121 changes: 121 additions & 0 deletions packages/durabletask-js/test/orchestration-state.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { OrchestrationState, OrchestrationStatus, FailureDetails } from "../src";
import { OrchestrationFailedError } from "../src/orchestration/exception/orchestration-failed-error";

describe("OrchestrationState", () => {
const instanceId = "test-instance-001";
const name = "TestOrchestrator";
const createdAt = new Date("2026-01-01T00:00:00Z");
const lastUpdatedAt = new Date("2026-01-01T00:01:00Z");

function createState(
runtimeStatus: OrchestrationStatus,
failureDetails?: FailureDetails,
): OrchestrationState {
return new OrchestrationState(
instanceId,
name,
runtimeStatus,
createdAt,
lastUpdatedAt,
undefined,
undefined,
undefined,
failureDetails,
);
}

describe("raiseIfFailed", () => {
it("should throw OrchestrationFailedError when failureDetails is present", () => {
const details = new FailureDetails("Something went wrong", "Error", "at foo.ts:1");
const state = createState(OrchestrationStatus.FAILED, details);

expect(() => state.raiseIfFailed()).toThrow(OrchestrationFailedError);
});

it("should include the instance ID and error message when failureDetails is present", () => {
const details = new FailureDetails("Something went wrong", "Error");
const state = createState(OrchestrationStatus.FAILED, details);

try {
state.raiseIfFailed();
throw new Error("Expected raiseIfFailed to throw");
} catch (e: unknown) {
expect(e).toBeInstanceOf(OrchestrationFailedError);
const error = e as OrchestrationFailedError;
expect(error.message).toContain(instanceId);
expect(error.message).toContain("Something went wrong");
expect(error.failureDetails).toBe(details);
}
});

it("should throw when runtimeStatus is FAILED but failureDetails is missing", () => {
const state = createState(OrchestrationStatus.FAILED);

expect(() => state.raiseIfFailed()).toThrow(OrchestrationFailedError);
});

it("should include the instance ID when runtimeStatus is FAILED without details", () => {
const state = createState(OrchestrationStatus.FAILED);

try {
state.raiseIfFailed();
throw new Error("Expected raiseIfFailed to throw");
} catch (e: unknown) {
expect(e).toBeInstanceOf(OrchestrationFailedError);
const error = e as OrchestrationFailedError;
expect(error.message).toContain(instanceId);
expect(error.failureDetails).toBeDefined();
expect(error.failureDetails.errorType).toEqual("UnknownError");
}
});

it("should not throw when runtimeStatus is COMPLETED", () => {
const state = createState(OrchestrationStatus.COMPLETED);

expect(() => state.raiseIfFailed()).not.toThrow();
});

it("should not throw when runtimeStatus is RUNNING", () => {
const state = createState(OrchestrationStatus.RUNNING);

expect(() => state.raiseIfFailed()).not.toThrow();
});

it("should not throw when runtimeStatus is TERMINATED", () => {
const state = createState(OrchestrationStatus.TERMINATED);

expect(() => state.raiseIfFailed()).not.toThrow();
});

it("should not throw when runtimeStatus is PENDING", () => {
const state = createState(OrchestrationStatus.PENDING);

expect(() => state.raiseIfFailed()).not.toThrow();
});

it("should not throw when runtimeStatus is SUSPENDED", () => {
const state = createState(OrchestrationStatus.SUSPENDED);

expect(() => state.raiseIfFailed()).not.toThrow();
});

it("should prioritize failureDetails over synthetic details when both are available", () => {
const details = new FailureDetails("Specific error", "CustomError", "stack trace");
const state = createState(OrchestrationStatus.FAILED, details);

try {
state.raiseIfFailed();
throw new Error("Expected raiseIfFailed to throw");
} catch (e: unknown) {
const error = e as OrchestrationFailedError;
// Should use the real failure details, not synthetic ones
expect(error.failureDetails.message).toEqual("Specific error");
expect(error.failureDetails.errorType).toEqual("CustomError");
expect(error.failureDetails.stackTrace).toEqual("stack trace");
}
});
});
});
Loading