This article mainly introduces object status and operation legality exceptions, which mainly include four types: InvalidOperationException, ObjectDisposedException, NotSupportedException, NotImplementedException, PlatformNotSupportedException, and TimeoutException.
〇.Foreword
The previous article has listed all the common abnormal types. This article continues to introduce the abnormal characteristics of each category.
This article mainly introduces object status and operation legality anomalies, mainly including the following types:InvalidOperationException、ObjectDisposedException、NotSupportedException、NotImplementedException、PlatformNotSupportedException、TimeoutException。
Click the link to view this series of articles:C# Exception detailed explanation series.
1. Overview of parameter verification exceptions
The common point of such exceptions is that the caller’s code syntax is correct and the parameters are correct, but the “timing” or “environment” is wrong.Mainly deal with the following three issues——Is the subject still alive? Is the object now in a state that allows this operation? Do you have this ability? The essence of selection is to distinguish between these three.
| Exception type | core meaning | permanent | inheritance relationship | Does retrying help? | preferred alternative |
|---|---|---|---|---|---|
| InvalidOperationException | Wrong status/timing | No (reordering may succeed) | Exception |
It depends on the situation | Disassemble the interface and state machine |
| ObjectDisposedException | released | yes | InvalidOperationException |
useless | life cycle governance |
| NotSupportedException | There is no such ability in design | yes | Exception |
useless | Change to narrow interface/CanXxx detection |
| NotImplementedException | Not written yet | No (will happen in the future) | Exception |
useless | Completion implementation |
| PlatformNotSupportedException | The current platform does not have this capability | Yes (just change the machine) | NotSupportedException |
useless | Runtime platform branch downgrade |
| TimeoutException | Waiting for timeout | No (retry may succeed) | Exception |
Caution (idempotence) | CancellationToken |
The three pairs that are most likely to be chosen incorrectly:
ArgumentExceptionvsInvalidOperationException: The former means “what you passed on is wrong”, and the latter means “what you passed on is right, but it can’t be done now”. Judgment criteria: Can the same parameters be successful at another time? Can → InvalidOperation.NotSupportedExceptionvsNotImplementedException: Design issues vs. progress issues. If you choose the opposite, the caller will make wrong decisions (such as bypassing your implementation, or waiting for you to release the version).TimeoutExceptionvsOperationCanceledException: Synchronous/protocol timeout vs asynchronous cancellation. The former is inSystem.Runtimethe latter inSystem.Threadingeven the namespace is different.
Key memory points: ObjectDisposedException inherits from InvalidOperationException; PlatformNotSupportedException inherits from NotSupportedException. Therefore, when catching the base class, the subclass will be captured “incidentally”, and if written in reverse order, more precise information will be swallowed up.
Here are a few general disciplines to note.
- Bring context: Write clearly “current status/desired status/which object/which platform/how many milliseconds to time out” in the message. These anomalies usually mean process defects online, and just looking at the type is not enough to locate them.
- XML document markup:
///so that the caller can see the preconditions in the IDE. - If it can be expressed using Boolean/enumeration/interface, do not use exceptions.:
TryOpen()、CanWrite、IReadOnlyListAll are better than “set and then catch”. - Don’t use them for control flow on hot paths:
foreachCatchNotSupportedExceptionJudging whether it can be written or not, the performance will be ugly. - The catch order is from fine to coarse:First
ObjectDisposedExceptionAgainInvalidOperationException;FirstPlatformNotSupportedExceptionAgainNotSupportedException。 - Custom exceptions should be restrained: These six types have covered 99% of status errors.It’s only worth deriving your own exception if “the caller needs to catch it separately and do special recovery”(And remember to set HResult and Serializable).
2. Introduction and simple examples of each exception
2.1 InvalidOperationException – “Not now”
Semantics:The object is in aLegal but not suitable for this operationstatus. It is not a permanent negation (change the timing and it will succeed), nor is it a parameter error. This is the most commonly used and easily abused category in this group.
Typical BCL cases:
SqlConnectionnot yetOpen()Just executeExecuteReaderIEnumerator.MoveNext()When it is found that the collection has been modified (detected internally by the version counter), throwInvalidOperationException:“Collection was modified; enumeration operation may not execute.”Liston readonly wrapper.Remove() - LINQ
Single()/First()on empty sequence;Enumerable.CastTransformation failed() - exist
foreachModify the collection in progress;DataTableWritten during BeginLoadData
// 一个简单的示例:
public class OrderProcessor
{
private enum State { Created, Started, Completed }
private State _state = State.Created;
private readonly List _items = new();
public void Start()
{
if (_state != State.Created)
throw new InvalidOperationException(
$"Order cannot be started in {_state} state.");
_state = State.Started;
}
public void AddItem(string item)
{
if (_state == State.Completed)
throw new InvalidOperationException("Order already completed.");
_items.Add(item);
}
public IEnumerable GetItems()
{
foreach (var it in _items) // 调用方若在此期间修改 _items,MoveNext 会抛 InvalidOperationException
yield return it;
}
}
Applicable scenarios:
- There is clearstate machineObjects (connections, transactions, orders, tasks, flows)
- The resource object that needs to call Init/Open/BeginXxx first
- Enumerators, iterators, delayed execution (
yield/ LINQ) legality check - The race condition of “having been completed by others first” under multi-threading (cooperating with
Interlockedjudgment)
Common misuses:
- Wrong parameter wrapping into it:
indexCrossing the line should be thrownArgumentOutOfRangeExceptionnot InvalidOperation. - Use it to cover up design flaws: If a method “often cannot be called”, it means that the interface is too fat and should be split into
ICanStart/IStartableWait for small interfaces to make unsupported states invisible at compile time. - There is no status information in the message. A good message should state “what is the current status and what is the expected status” to facilitate online troubleshooting.
2.2 ObjectDisposedException – “Already dead”
Semantics:The object’s life cycle has endedany member (except again Dispose ) should not be called. it is InvalidOperationException A subclass of , but with narrower and more diagnostic semantics.
Areas that require special attention:
DisposeMust be idempotentthe second call should not throw an exception; what is thrown is “other members are called after Dispose”.- Standard practice is to do a disposed check at the beginning of each public member; field
_disposedIt is recommended to usebool(simple scenario) or combinedGC.SuppressFinalize。 - .NET 7+ provided
ObjectDisposedException.ThrowIf(bool condition, object instance)it can be done in one line and the message format is unified. - Don’t throw from a finalizer; don’t expect managed objects to be available during finalization.
- Note on asynchronous scenarios:
DisposeAsyncThere is still await running later, which is a typical “released but still used”.
public sealed class LogWriter : IDisposable
{
private readonly StreamWriter _writer;
private bool _disposed;
public LogWriter(string path) => _writer = new StreamWriter(path, append: true);
public void Write(string message)
{
// .NET 7+ 推荐写法:ObjectDisposedException.ThrowIf(_disposed, this);
if (_disposed) throw new ObjectDisposedException(nameof(LogWriter));
_writer.WriteLine($"{DateTime.Now:O} {message}");
}
public void Flush()
{
if (_disposed) throw new ObjectDisposedException(nameof(LogWriter));
_writer.Flush();
}
public void Dispose()
{
if (_disposed) return; // 幂等
_writer.Dispose(); // 释放资源
_disposed = true; // 标志置位必须在释放之后(避免释放失败时误判为已释放)
}
}
// 调用方
var log = new LogWriter("app.log");
log.Dispose();
log.Write("oops"); // ObjectDisposedException: Cannot access a disposed object. Object name: 'LogWriter'.
Applicable scenarios:
- All implementations
IDisposable/IAsyncDisposabletype (stream, connection, handle wrapper, cache file, timer) - Return the pooled object before using it again
- HttpClient is reused after Dispose (a very common online bug)
Trade-off with InvalidOperationException.If the type is not implemented IDisposablejust use InvalidOperationException。ObjectDisposedException is the exclusive signal of Dispose modeit can directly point to life cycle management issues when monitoring alarms and log retrieval, so it is worth using alone.
2.3 NotSupportedException – “Not by design”
Semantics:The member exists in the interface/base class contract, butThe current implementation is by design unable to provide. This is not a temporary state, nor will it be retried ten thousand times.
Typical BCL cases:
Streamread onlyCanWrite == falsecallWrite→NotSupportedExceptionArray/ Fixed size list implementationIList.Add→NotSupportedExceptionReadOnlyCollection.Add Encoding.GetEncoding("unsupported")Some scenarios (more ArgumentException)ICollectionHow to modify the time.IsReadOnly == true
public class ReadOnlyStream : Stream
{
private readonly Stream _inner;
public ReadOnlyStream(Stream inner) => _inner = inner;
public override bool CanRead => true;
public override bool CanWrite => false; // 关键:先提供探测属性
public override bool CanSeek => _inner.CanSeek;
public override long Length => _inner.Length;
public override long Position { get => _inner.Position; set => throw new NotSupportedException(); }
public override int Read(byte[] buffer, int offset, int count) => _inner.Read(buffer, offset, count);
public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException("This stream is read-only.");
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin);
public override void SetLength(long value) => throw new NotSupportedException();
}
Applicable scenarios:
- Implementing a “wide interface” (
IList、IDbCommand、Stream、ICollection) is a subset of capabilities - Missing low-level capabilities in adapter/decorator pattern
- Some operations in abstract base classes are meaningless to some subclasses
Best practice: Don’t let the caller rely on exception programming if you can detect it.The standard mode of BCL is CanXxx attribute + throw exceptionDouble insurance:
// 推荐:先问,再做
if (stream.CanWrite)
stream.Write(buf, 0, len);
// 不推荐:做了再说,靠异常当分支
try { stream.Write(buf, 0, len); }
catch (NotSupportedException) { /* 哦,原来不能写 */ }
The document should clearly state “which members will be thrown” and mark it in the XML comments. 。
Reflection on the design level.If you find yourself often Add/Remove throw NotSupportedExceptiongive priority toChange interface:use IReadOnlyList replace IList,use IEnumerable replace ICollection. Letting compile-time and IDE prompts prevent illegal calls is an order of magnitude better than throwing exceptions at runtime.
2.4 NotImplementedException – “Not done yet”
Semantics:code pathDoes not exist yetbelongs to the development period. The difference between it and “not supported” is the intent:There will be in the future, but not now。
HResult is 0x80004001 (E_NOTIMPL),and NotSupportedException yes 0x80131515——It can be separated at a glance in the debugger.
public class PaymentService : IPaymentService
{
public Task PayAsync(decimal amount) => throw new NotImplementedException("微信支付尚未接入,预计 Q4 完成");
// IDE 自动生成接口实现时默认就填这个,方便你逐个实现
public Task RefundAsync(string id) => throw new NotImplementedException();
}
Applicable scenarios:
- After IDE generates interface/overridden methodPlaceholder(This is its main and most legitimate use)
- When delivering large functions in iterations, let the project compile and pass first
- Rare case: the base class provides optional hooks and subclasses do not need to override them (this is more recommended
virtualEmpty implementation or abstract class)
Where not to use it:
- should never appear in the normal running path of production code. It must be cleared before going online, otherwise the user will see a
NotSupportedExceptionMore difficult to understand errors. - Don’t use it to mean “this subclass will never have this ability” – that’s
NotSupportedException。 - Don’t use it to express “the parameter combination has not been processed yet” – that is
ArgumentExceptionorInvalidOperationException。
Memory tips:NotImplemented = timing issue; NotSupportedException = design issue。
2.5 PlatformNotSupportedException – “This machine cannot run”
Semantics: The API is compiled in (usually a shared code base/multi-targeting), but the underlying capabilities are not provided by the current OS or runtime. it is NotSupportedException subclass, so when catching the parent class, it will be captured together – pay attention to the catch order.
Typical BCL cases:
Microsoft.Win32.RegistryOn non-Windows platformsProcessStartInfo.Verb = "runas"(Elevated privileges) on non-WindowsWindowsIdentity/WindowsPrincipalIn LinuxConsole.Beep()In some Unix terminalsRfc2898DeriveBytesCertain hardware acceleration paths forVectorHardware intrinsics are not supported on CPUs- Call desktop-specific API in MAUI / Blazor Hybrid
using System.Runtime.InteropServices;
public static class OsHelper
{
public static void SetRegistryValue(string key, string value)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new PlatformNotSupportedException(
"Registry is only available on Windows. Use a config file on Linux/macOS.");
Microsoft.Win32.Registry.CurrentUser.CreateSubKey(key).SetValue("App", value);
}
// 更好的做法:主动降级而不是抛
public static string GetConfigPath() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "app.cfg")
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".app.cfg");
}
Applicable scenarios:
- Shared libraries for multi-target frameworks (netstandard/net8.0+windows)
- Preserve old Windows-specific API stubs during containerization/cross-platform migration
- conditional compilation (
#if WINDOWS) other than runtime branches
Best practices:
- priority
RuntimeInformation.IsOSPlatform/OperatingSystem.IsWindows()Make an active branchgracefully degrade or return to alternatives, and don’t let users run into the depths before hitting exceptions. - The message states “Which platform supports it and what alternatives are available for the current platform”.
- Used for unit testing
[Fact(Skip = ...)]orOperatingSystemGuard to prevent CI from running Windows use cases on Linux.
2.6 TimeoutException – “Waiting too long”
Semantics:The operation was not completed within the agreed time.
Notice:it meansWait for strategy to be exhausteddoes not mean that the other party failed, nor does it mean that the operation did not occur.(It may already be executing, and repeated retries may cause problems such as double deductions).
public async Task FetchWithTimeoutAsync(HttpClient client, Uri uri, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
try
{
return await client.GetStringAsync(uri, cts.Token);
}
catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested)
{
// HttpClient 超时会抛出 TaskCanceledException(派生自 OperationCanceledException)
throw new TimeoutException($"GET {uri} timed out after {timeout.TotalSeconds}s.");
}
}
// 同步/协议级超时:串口、WCF、自定义握手
public byte[] ReadExactly(SerialPort port, int count, int millisecondsTimeout)
{
var buf = new byte[count];
var end = DateTime.UtcNow.AddMilliseconds(millisecondsTimeout);
int read = 0;
while (read < count)
{
if (DateTime.UtcNow > end)
throw new TimeoutException($"Expected {count} bytes, got {read}.");
port.ReadTimeout = Math.Max(1, (int)(end - DateTime.UtcNow).TotalMilliseconds);
read += port.Read(buf, read, count - read);
}
return buf;
}
Applicable scenarios:
- Synchronous blocking IO, serial port/socket reading and writing, protocol handshake
- Distributed lock/semaphore wait timeout (
SemaphoreSlim.Wait(timeout)Can be re-thrown when false is returned) - Client-side encapsulation of external system calls (putting the underlying
TaskCanceledExceptionUniformly translated into business readableTimeoutException) - “Don’t wait if you can’t get it” in cache penetration protection
Important Pitfalls: TPL / Async Cancellation Don’t use it:
CancellationTokentimeout → throwOperationCanceledException/TaskCanceledExceptionHttpClienttimeout →TaskCanceledExceptionTask.Wait/Resulttimeout → return false or package inAggregateExceptioninside
That is to say:TimeoutException is the language of “protocol/sync-wait”,OperationCanceledException Is the language of “asynchronous cancellation”. Mixing them will make all catch logic invalid. Additionally, capture TimeoutException Before trying again, be sure to ask yourself: Has the upstream already taken effect? (Impotence/compensation mechanism)
3. Compare the above anomalies in a real scene
The following is an imaginary “message sender” with four types of exceptions performing their own duties:
public class SmtpSender : IDisposable
{
private bool _disposed;
private bool _connected;
private readonly int _timeoutMs;
public SmtpSender(int timeoutMs = 5000) => _timeoutMs = timeoutMs;
public void Connect()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_connected) throw new InvalidOperationException("Already connected.");
// ... 真正连
_connected = true;
}
public void Send(string to, string body)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!_connected) throw new InvalidOperationException("Send requires Connect() first.");
// ... 发送,带超时
// 组合:调用方超时 + 本类默认超时
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
linkedCts.CancelAfter(_timeoutMs);
try
{
await _client.SendMailAsync(to, body, linkedCts.Token);
}
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
{
// 是自己的超时触发的 → 转成业务可读的 TimeoutException
throw new TimeoutException($"Send timed out after {_timeoutMs}ms.", ex);
}
// 调用方主动取消:原样抛出 OperationCanceledException,不要包成 TimeoutException
}
// 这个实现只会发文本,附件从设计上就不支持
public virtual void SendWithAttachment(string to, string body, string path) =>
throw new NotSupportedException("Attachments are not supported by this sender.");
// 下个迭代才接的模板功能
public void SendTemplate(string templateId, object model) =>
throw new NotImplementedException("Template sending planned for v2.");
// 只在 Windows 上可用的 SSO 登录
public void LoginWithCurrentUser()
{
if (!OperatingSystem.IsWindows())
throw new PlatformNotSupportedException("SSPI login requires Windows.");
}
public void Dispose()
{
if (_disposed) return;
_connected = false;
_disposed = true;
}
}
Each of the five exceptions in the same class belongs to its own place, and the caller can judge “should I reconnect, change the implementation, upgrade the version, or change the operating system” by relying on the type.
4. A small summary
These six exceptions handle the situation of “the parameters are correct, but the call still fails”. The core is to distinguish whether the cause of the failure is that the object is still alive, whether the ability exists, or whether the timing is right.
Can be divided into three groups according to properties:
- The only life cycle problem is ObjectDisposedExceptionindicating that the object has been released and cannot be restored, provided that the type implements IDisposable, otherwise InvalidOperationException should be returned;
- Capability issues include NotSupportedException (not supported by design), NotImplementedException (not finished yet, should be cleared before going online), and PlatformNotSupportedException (not supported by the current platform). All three are permanent failures, and retrying is meaningless;
- Timing and environment issues are InvalidOperationException (the status or sequence is wrong, it may succeed at another time) and TimeoutException (the waiting strategy is exhausted, which does not mean that the operation has not occurred).
When selecting, ask yourself: Is the object released? Is the capability not in the design, not written yet, or is it not supported by the platform? Is the status wrong? Have you waited too long?Each answer corresponds to a different direction of repair: managing the life cycle, narrowing the interface or completing the implementation, correcting the call sequence, and improving the timeout and cancellation strategy.
Three common mistakes need to be noted:The difference between ArgumentException and InvalidOperationException lies in whether the same parameter can be successfully changed at another time; the latter two are design issues and progress issues, and choosing the opposite will mislead the caller; use TimeoutException for synchronous timeout, and OperationCanceledException for asynchronous cancellation. Mixed use will cause the catch logic to fail.In addition, the inheritance relationship determines that the catch order must be from fine to coarse, capturing the subclass first and then the parent class.