- Use Promises to run individual durable operations or calls to other handlers concurrently.
- Use Durable Coroutines when each concurrent task contains multiple durable steps that should run in the same handler, for example to share local clients and connections.
Concurrency with Promises
Parallelizing tasks
Start multiple durable operations concurrently by calling them without immediately awaiting:Retrieving results
Restate provides several patterns for coordinating concurrent tasks. All patterns useRestatePromise combinators that log the order of completion, ensuring deterministic behavior during replays.
Wait for all tasks to complete
Similar toPromise.all, waits for all promises to resolve successfully:
Wait for the first successful completion
Similar toPromise.any, returns the first promise that resolves successfully:
Wait for the first to complete
Similar toPromise.race, returns the first promise to settle (resolve or reject):
Wait for all to settle
Similar toPromise.allSettled, waits for all promises to complete regardless of success or failure:
Creating already-completed promises
You can createRestatePromise instances that are already resolved or rejected, mirroring Promise.resolve and Promise.reject:
RestatePromise.
Detecting RestatePromises
UseisRestatePromise to check whether a value is a RestatePromise before using it with combinators like RestatePromise.all():
Durable Coroutines
Durable Coroutines let you run concurrent tasks that each contain multiple durable steps within one handler. Each coroutine can pause while waiting for a result, let other coroutines make progress, and resume after failures through replay. Use them when your application benefits from keeping concurrent work in the same process. For example, an agent can run several tools, each with a guardrail check followed by an API call, while sharing local clients and connections. Or it can coordinate background work with steering input and approvals.Install the coroutine package
Install the coroutine package alongside the TypeScript SDK:Using an existing SDK handler
Callexecute(ctx, operation) from your handler to run a coroutine. It returns a promise with the result, so you can await it alongside your existing handler logic. This works in Services, Virtual Objects, and Workflows:
loadUserSummary is a coroutine that loads a user and their orders. The next section defines it.
Composing durable steps
Coroutines use TypeScript generators (function*). Use gen to describe a routine and yield* to wait for each durable operation. The SDK controls when each generator resumes so it can replay concurrent work consistently.
This coroutine loads a user, then their orders:
fetchUser and fetchOrders stand in for your application APIs. Each call has its own journal entry. yield* pauses the coroutine until a result is available.
Keep external I/O and other nondeterministic work inside run. Its callback can use async and await, and receives { signal }, where signal is an AbortSignal you can pass to APIs such as fetch for cancellation.
Give each run a name with { name: "fetch-user" }, or pass a named function. Anonymous callbacks require an explicit name.
Call coroutine operations such as
run, sleep, and spawn from inside the generator body. Do not call them from a run callback or an unrelated asynchronous callback. As with ctx.run, a run callback must not perform Restate context operations.Running concurrent coroutines
Usespawn to start a coroutine and get a Task<T> representing its result. This handler starts one coroutine per user and uses all to collect their results in input order:
Operations and futures
The coroutine API distinguishes between a description of work and work that has started:
A raw generator instance can only be consumed once. Use
gen with a generator factory when you want to execute the same operation again.
Use the coroutine combinators for these futures, rather than native Promise combinators. For a group of individual side effects, create run futures and pass them directly to all. Use spawn when each task consists of multiple steps, as in loadUserSummary.
Combining results
Use these combinators inside a coroutine. They accept futures from durable operations, spawned coroutines, or a mix of both.Wait for all results
all returns results in input order. If an input fails, it throws that error. This example runs two side effects concurrently:
Wait for the first result
race returns the first result or throws the first error. To also identify which branch finished, use select:
Wait for the first successful result
any returns the first successful result, even if other inputs fail. If every input fails, it throws an AggregateError containing their errors:
Wait for all results and errors
allSettled waits for every input and returns its outcome in input order. Each entry has either status: "fulfilled" and a value, or status: "rejected" and a reason:
Identify which branch finished first
select returns the tag and future of the first branch to finish. The tag is the key you gave that branch: "first" or "second" in this example. Switch on winner.tag to handle each branch, then yield its future to retrieve the result or throw its error:
race, any, and select do not interrupt losing coroutines. They can continue while the main operation runs. See Coroutine lifetime for what happens when it finishes.
Timeouts
Inside a coroutine, useselect with a durable timer to limit how long you wait for a result. Here, budgetSeconds sets the timeout, and winner.tag is "done" when the summary finishes first, or "timeout" when the timer finishes first:
Sharing values between coroutines
UsecontextLocal to share values such as a request ID or tenant with nested helpers and spawned coroutines, without passing them through every function.
Define the storage at module scope, then read and write it inside generator code. This helper reads the request ID before calling an external audit service:
execute call share the value. Separate calls have their own storage, even in the same process. get() returns the supplied default until you call set, or undefined if you did not supply a default.
Context local values live in memory. Set them from input or a journaled result so replay reconstructs the same values. Use durable state for values that must persist across invocations.
Call
get and set in generator code, outside run callbacks. The value is shared by all coroutines in that execution, so a later set changes what the other coroutines read.Errors and retries
run uses the SDK’s error handling and retries. Configure retries through its retry option, using fields such as maxAttempts, maxDuration, and initialInterval. When the retry policy is exhausted, the operation throws a TerminalError at yield*.
Use try/catch around yield* to handle a failure or perform compensation with additional durable steps. Throw a TerminalError from the callback for failures that should not be retried. See the Sagas guide for compensation patterns.
Coroutine lifetime
By default, execution finishes when the main operation returns or throws, both for generator handlers and when usingexecute. Any unfinished spawned coroutines are abandoned: they are no longer resumed, and their catch and finally blocks do not run. Durable work already performed remains in the journal.
To ensure a coroutine finishes, yield its task before returning, as all(tasks) does above. Alternatively, pass { onMainExit: "join" } as the third argument to execute to wait for all spawned coroutines. With this option, a coroutine waiting indefinitely also keeps the handler running indefinitely.
Interrupting a coroutine
Calltask.interrupt() to throw an InterruptedError at the coroutine’s next yield point and abort the signal passed to its active run callback. Pass that signal to external APIs that support cancellation, otherwise the external I/O can continue. You can pass a custom error to interrupt(error) to throw that error instead.
Interruption also propagates to coroutines spawned by that task, and the coroutine can catch the error to perform cleanup or recover.
After interrupting, yield the task and handle its error to let its cleanup run. Interrupting and immediately returning from the main operation can abandon the task before its catch or finally executes.
In this example, the task records an audit entry in finally before the caller catches the interruption:
Cooperative cancellation with a stop channel
Use a stop channel to let a coroutine finish its current work and return partial results. This worker checks for a stop before each summary:next.tag identifies whether the stop request or delay finished first. The main coroutine below sends a stop when winner.tag is "timeout", then yields the worker so it can finish:
Using other Restate features
Inside a coroutine, use these functions for common Restate operations:clientandsendClientcall other Services, Virtual Objects, and Workflows.stateandsharedStateaccess durable state from handlers with the corresponding state access permissions.awakeablecreates an awakeable, a convenient shorthand for a one shot signal with a generated ID that can be completed through the SDK or Restate HTTP API.workflowPromiseaccesses a named workflow promise, resolved once and retrievable during workflow retention.
Generator handlers
You can also write a whole handler as a coroutine, without callingexecute yourself.
Getting started with generator handlers
Follow the TypeScript prerequisites. For a new project, start with the template:Defining generator handlers
Use the coroutine package’sservice helper to define generator handlers. Operation<T> describes the handler’s result type, and yield* waits for a durable operation and retrieves its result:
fetchUser represents your API or database call. run records its result, like ctx.run, so completed calls are not repeated during replay. Serve it with serve from @restatedev/restate-sdk, and register it as described in Serving.
Use restate.object and restate.workflow in the same way to define Virtual Objects and Workflows with generator handlers.