Must call EndRead in ALL cases?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In the classic .NET asynchronous programming model, BeginRead starts an operation and EndRead finishes it. If BeginRead returned successfully, the safe rule is that you must call EndRead exactly once for that operation, even when the read fails.
Why BeginRead and EndRead are a pair
The older APM pattern splits one logical read into two API calls. BeginRead queues the I/O work and gives you an IAsyncResult. EndRead completes the contract by retrieving the byte count, surfacing deferred exceptions, and letting the stream release any internal state associated with the operation.
That last point matters more than many examples suggest. A read can appear to start correctly and still fail later because the socket closes, the file handle becomes invalid, or the stream is disposed before completion. In APM, those failures often show up only when EndRead executes. If you skip EndRead, you are not just ignoring a return value. You are leaving the operation unfinished.
There is one narrow exception. If BeginRead throws before it returns an IAsyncResult, there is nothing to complete. In every other normal path, successful start means required completion.
A safe callback pattern
The main discipline is simple: the callback that owns the IAsyncResult should also own the matching EndRead call. Keep both steps close together so the code cannot accidentally complete the same operation twice or forget to complete it at all.
This pattern makes ownership obvious. BeginRead starts one operation, and the callback calls EndRead once inside a try block so deferred exceptions are handled in the same place.
Handling failures and cancellation paths
Developers often ask whether failure paths let them skip EndRead. Usually they do not. If the callback ran, you should assume an operation exists and complete it. EndRead is exactly where you learn whether the read produced bytes, hit end of stream, or failed.
The confusing part is that APM does not map well onto modern cancellation patterns. Many APM APIs were designed before CancellationToken became common, so cancellation often means closing the underlying stream or abandoning the workflow. Even then, the callback may still fire. The correct callback still attempts EndRead and handles the expected exception.
A useful mental model is this: BeginRead reserves the right for the stream to answer later, and EndRead is where you collect that answer.
Why modern async code is easier
New .NET code should usually avoid APM entirely. ReadAsync and await express the same intent without requiring manual pairing rules or callback ownership conventions.
With await, the completion step is built into the asynchronous method call. You still need normal error handling, but you no longer have to remember an extra EndRead call for each pending read. That makes the code easier to audit and much harder to misuse.
Common Pitfalls
- Treating the callback as proof that the read already succeeded. The callback only means the operation finished;
EndReadtells you how. - Ignoring
EndReadwhen you do not need the byte count. Even if the count is irrelevant, the completion step is still required. - Calling
EndReadfrom more than one branch. OneBeginReadmaps to oneEndRead, no more and no less. - Assuming every error would have been thrown by
BeginRead. Many I/O failures are deferred until completion. - Writing new code with APM out of habit. In most current codebases,
ReadAsyncis clearer and safer.
Summary
- In APM,
BeginReadandEndReadform one operation. - If
BeginReadreturns anIAsyncResult, callEndReadexactly once. - '
EndReadreturns the byte count and surfaces deferred exceptions.' - The only normal case without
EndReadis whenBeginReaditself throws before the operation starts. - Prefer
ReadAsyncandawaitin new code, but keep the pairing rule in mind when maintaining older APIs.

