{"record":{"id":"26f1e5ae695d2365","repo":"dotnet/orleans","slug":"withdrawing-amount-credits-from-account-this-g","errorCode":null,"errorMessage":"Withdrawing {amount} credits from account \"{this.GetPrimaryKeyString()}\" would overdraw it. This account has {balance.Value} credits.","messagePattern":"Withdrawing (.+?) credits from account \"(.+?)\" would overdraw it\\. This account has (.+?) credits\\.","errorType":"validation","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"samples/BankAccount/AccountTransfer.Grains/AccountGrain.cs","lineNumber":30,"sourceCode":"\npublic sealed class AccountGrain : Grain, IAccountGrain\n{\n    private readonly ITransactionalState<Balance> _balance;\n\n    public AccountGrain(\n        [TransactionalState(\"balance\")] ITransactionalState<Balance> balance) =>\n        _balance = balance ?? throw new ArgumentNullException(nameof(balance));\n\n    public Task Deposit(int amount) =>\n        _balance.PerformUpdate(\n            balance => balance.Value += amount);\n\n    public Task Withdraw(int amount) =>\n        _balance.PerformUpdate(balance =>\n        {\n            if (balance.Value < amount)\n            {\n                throw new InvalidOperationException(\n                    $\"Withdrawing {amount} credits from account \" +\n                    $\"\\\"{this.GetPrimaryKeyString()}\\\" would overdraw it.\" +\n                    $\" This account has {balance.Value} credits.\");\n            }\n\n            balance.Value -= amount;\n        });\n\n    public Task<int> GetBalance() =>\n        _balance.PerformRead(balance => balance.Value);\n}\n","sourceCodeStart":12,"sourceCodeEnd":42,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/samples/BankAccount/AccountTransfer.Grains/AccountGrain.cs#L12-L42","documentation":"An InvalidOperationException thrown inside the AccountGrain.Withdraw transactional update when the requested withdrawal amount exceeds the current balance. The grain uses ITransactionalState<Balance>.PerformUpdate so the check executes atomically; if it throws, the transaction aborts and rolls back, leaving the balance unchanged. The message includes the grain's string key and the live balance for diagnosis.","triggerScenarios":"Calling IAccountGrain.Withdraw(amount) where amount > current balance.Value inside the PerformUpdate lambda. Because it is a transactional state update, the read of balance.Value is consistent within the transaction.","commonSituations":"A caller (transfer sender) requests more than the account holds. Concurrent withdrawals racing on the same account — the transactional guard serializes them and one will overdraw against the now-lower balance. A bug computing the amount.","solutions":["Have the caller call GetBalance() first (or check within the same transaction) and reject/handle insufficient-funds before calling Withdraw.","Catch InvalidOperationException at the transfer/caller layer and surface an 'insufficient funds' result to the user instead of crashing.","For transfers, use Orleans transactions across both accounts so the debit+credit is atomic and a failed debit cancels the credit."],"exampleFix":"// before\npublic Task Withdraw(int amount) =>\n    _balance.PerformUpdate(balance =>\n    {\n        if (balance.Value < amount)\n            throw new InvalidOperationException($\"Withdrawing {amount} ... overdraw it.\");\n        balance.Value -= amount;\n    });\n\n// after (return a result instead of throwing for expected business failures)\npublic Task<bool> TryWithdraw(int amount) =>\n    _balance.PerformUpdate(balance =>\n    {\n        if (balance.Value < amount) return false;\n        balance.Value -= amount;\n        return true;\n    });","handlingStrategy":"validation","validationCode":"var balance = await account.GetBalance();\nif (balance < amount)\n    return Results.BadRequest(\"Insufficient funds\");\nawait account.Withdraw(amount);","typeGuard":null,"tryCatchPattern":"try {\n    await account.Withdraw(amount);\n} catch (InvalidOperationException ex) when (ex.Message.Contains(\"overdraw\")) {\n    // surface as a domain result, not a crash\n    return Results.BadRequest(\"Insufficient funds\");\n}","preventionTips":["Pre-check the balance before withdrawing for a friendlier UX.","Prefer returning a result (TryWithdraw) over throwing for expected business failures.","Use Orleans transactions for transfers so debit+credit are atomic and consistent."],"tags":["orleans","transactional-state","business-logic","validation","grain"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}