Skip to content

azure-functions-durable (v4 compat): Task/TimerTask missing v3 members (result/isCompleted/isFaulted, cancel/isCanceled) #292

Description

@YunchuWang

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)

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions