Summary
The durable-functions v4 compat layer (packages/azure-functions-durable) exposes the core Task / TimerTask objects unchanged to classic (v3) orchestrator bodies. But the core v4 Task surface dropped several members that the v3 public Task / TimerTask interfaces had, so a common, previously-canonical v3 pattern — Task.any([activity, timer]), read the winner, cancel the loser — no longer type-checks or runs.
Symptom
The classic "activity vs. timeout" idiom fails to compile against v4:
TimeoutOrchestration.ts(22,21): error TS2339: Property 'cancel' does not exist on type 'Task<unknown>'.
TimeoutOrchestration.ts(23,29): error TS2551: Property 'result' does not exist on type 'Task<unknown>'. Did you mean '_result'?
Offending v3 code:
const winner = yield context.df.Task.any([activityTask, timeoutTask]);
if (winner === activityTask) {
timeoutTask.cancel(); // v4: TimerTask has no cancel()
return activityTask.result; // v4: Task has no .result (only getResult())
}
v3 vs. v4 shape
v3 public (azure-functions-durable-js types/task.d.ts) |
v4 core (packages/durabletask-js task/task.ts) |
Task.isCompleted: boolean |
Task.isComplete: boolean (getter) |
Task.isFaulted: boolean |
Task.isFailed: boolean (getter) |
Task.result?: unknown (getter) |
Task.getResult(): T (method; throws if not complete) |
TimerTask.isCanceled: boolean |
— (none) |
TimerTask.cancel(): void |
— (none) |
The compat DurableOrchestrationContext (packages/azure-functions-durable/src/orchestration-context.ts) returns core Tasks directly from createTimer / callActivity / etc., so none of the v3 member names are present.
Constraint worth calling out
result, isCompleted, and isFaulted are trivial, safe aliases of existing core members (getResult() — returning undefined instead of throwing when incomplete — isComplete, isFailed).
cancel() / isCanceled are not just naming: core createTimer returns a plain CompletableTask and core has no timer-cancellation primitive at all. A compat-only cancel() can make v3 code compile and let the activity-wins branch return, but it will not truly cancel the pending timer, so the orchestration may stay alive until the timer fires. Correct cancellation semantics require a core cancellable-timer primitive.
Affected
- The classic
Task.any([activity, timeout]) cancel-the-loser pattern (activity-wins branch). The timeout-wins branch does not touch these members and is unaffected.
Suggested fix (options)
- Compat alias (safe, partial): add
result (getter → getResult() guarded by isComplete), isCompleted, isFaulted on the tasks the compat layer hands to classic orchestrators. Low risk, restores compile + read semantics.
- Timer cancellation (needs core): add a cancellable-timer primitive to core
createTimer / TimerTask, then surface cancel() / isCanceled through the compat TimerTask. This is the piece that requires a real core change rather than an alias.
- Or, if these removals are intentional, document the v3→v4 migration (
.result → .getResult(); replacement for timer.cancel()) in the upgrade notes, since this was a documented canonical pattern.
Ideally 1 + 2 so existing v3 orchestrators using the documented idiom run unchanged.
Environment
Surfaced by the classic v3 sample suite (a timeout/Task.any orchestration) running on the v4 durable-functions compat layer over gRPC against a local WebJobs.Extensions.DurableTask host.
Summary
The
durable-functionsv4 compat layer (packages/azure-functions-durable) exposes the coreTask/TimerTaskobjects unchanged to classic (v3) orchestrator bodies. But the core v4Tasksurface dropped several members that the v3 publicTask/TimerTaskinterfaces had, so a common, previously-canonical v3 pattern —Task.any([activity, timer]), read the winner, cancel the loser — no longer type-checks or runs.Symptom
The classic "activity vs. timeout" idiom fails to compile against v4:
Offending v3 code:
v3 vs. v4 shape
azure-functions-durable-jstypes/task.d.ts)packages/durabletask-jstask/task.ts)Task.isCompleted: booleanTask.isComplete: boolean(getter)Task.isFaulted: booleanTask.isFailed: boolean(getter)Task.result?: unknown(getter)Task.getResult(): T(method; throws if not complete)TimerTask.isCanceled: booleanTimerTask.cancel(): voidThe compat
DurableOrchestrationContext(packages/azure-functions-durable/src/orchestration-context.ts) returns coreTasks directly fromcreateTimer/callActivity/ etc., so none of the v3 member names are present.Constraint worth calling out
result,isCompleted, andisFaultedare trivial, safe aliases of existing core members (getResult()— returningundefinedinstead of throwing when incomplete —isComplete,isFailed).cancel()/isCanceledare not just naming: corecreateTimerreturns a plainCompletableTaskand core has no timer-cancellation primitive at all. A compat-onlycancel()can make v3 code compile and let the activity-wins branch return, but it will not truly cancel the pending timer, so the orchestration may stay alive until the timer fires. Correct cancellation semantics require a core cancellable-timer primitive.Affected
Task.any([activity, timeout])cancel-the-loser pattern (activity-wins branch). The timeout-wins branch does not touch these members and is unaffected.Suggested fix (options)
result(getter →getResult()guarded byisComplete),isCompleted,isFaultedon the tasks the compat layer hands to classic orchestrators. Low risk, restores compile + read semantics.createTimer/TimerTask, then surfacecancel()/isCanceledthrough the compatTimerTask. This is the piece that requires a real core change rather than an alias..result→.getResult(); replacement fortimer.cancel()) in the upgrade notes, since this was a documented canonical pattern.Ideally 1 + 2 so existing v3 orchestrators using the documented idiom run unchanged.
Environment
Surfaced by the classic v3 sample suite (a timeout/
Task.anyorchestration) running on the v4durable-functionscompat layer over gRPC against a local WebJobs.Extensions.DurableTask host.