dotnet/orleans · error · InvalidOperationException
Balance has not been written.
Error message
Balance has not been written.
What it means
Thrown inside CreateSummary when balance.Value is null — the IDurableValue<AccountBalance> durable value was never assigned before the summary is built. CreateSummary is called from both RunScenario (which sets balance.Value first) and GetSummary, so calling GetSummary on an activation that never ran the scenario trips it.
Source
Thrown at samples/JournalingAzureBlobJson/JournalingAzureBlobJson/Program.cs:317
public Task Deactivate()
{
DeactivateOnIdle();
return Task.CompletedTask;
}
private JournaledSampleSummary CreateSummary()
{
var completion = receipt.State;
return new JournaledSampleSummary(
_activationId,
inventory.OrderBy(static item => item.Key, StringComparer.Ordinal)
.Select(static item => new InventoryEntry(item.Key, item.Value))
.ToArray(),
events.ToArray(),
workQueue.ToArray(),
tags.Order(StringComparer.Ordinal).ToArray(),
balance.Value ?? throw new InvalidOperationException("Balance has not been written."),
profile.State,
completion.Status,
completion.Value);
}
}
[GenerateSerializer]
public sealed record JournaledSampleSummary(
[property: Id(0)] Guid ActivationId,
[property: Id(1)] InventoryEntry[] Inventory,
[property: Id(2)] JournalEvent[] Events,
[property: Id(3)] WorkItem[] WorkQueue,
[property: Id(4)] string[] Tags,
[property: Id(5)] AccountBalance Balance,
[property: Id(6)] ProfileState Profile,
[property: Id(7)] DurableTaskCompletionSourceStatus CompletionStatus,
[property: Id(8)] Receipt? Receipt);
View on GitHub (pinned to fca799fa70)
Solutions
- Call grain.RunScenario() first — it sets balance.Value before WriteStateAsync — then call GetSummary.
- If you want GetSummary to be safe on cold activations, guard with `if (balance.Value is null) return ...` or return an empty/error summary instead of throwing.
- Ensure the append blob was not reset between RunScenario and GetSummary, otherwise recovery yields a null balance.
Example fix
// before var summary = await grain.GetSummary(); // throws on cold activation // after: run the scenario that initializes balance before summarizing await grain.RunScenario(); var summary = await grain.GetSummary();
Defensive patterns
Strategy: validation
Validate before calling
// Guard GetSummary-style calls against an unwritten balance
var summary = (balance.Value is null)
? null
: CreateSummary(); Type guard
// Narrow a nullable durable value before use
static bool TryGetBalance(IDurableValue<AccountBalance> balance, out AccountBalance value)
{
value = balance.Value!;
return value is not null;
} Try / catch
null
Prevention
- Call RunScenario before GetSummary so balance.Value is populated.
- Prefer null-safe summary construction over throwing from a getter.
- Do not clear the append blob between RunScenario and GetSummary.
When it happens
Trigger: A client calls grain.GetSummary() on a fresh activation (or one whose RunScenario was never invoked), so balance.Value is still its default null. CreateSummary evaluates `balance.Value ?? throw new InvalidOperationException(...)` and the null-coalesce fires.
Common situations: Restarting the silo against an empty/cleared blob and querying the summary before running the scenario; a second client calling GetSummary while the first has not finished RunScenario; changing the sample flow so balance is written conditionally.
Related errors
- Recovered {name} does not match. Written: {Serialize(written
- This grain is already producing events
- The grain did not reactivate after DeactivateOnIdle.
- Recovered {name} count does not match. Written: {written.Cou
- Recovered {name} item {i} does not match. Written: {Serializ
AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13).
Data as JSON: /api/errors/4b3386c29ab52f18.
Report an issue: GitHub.