UnhandledExceptionHandling
The UnhandledExceptionHandlingAttribute specifies how NUnit should handle unhandled exceptions that occur on
background threads during test execution. By default, NUnit treats unhandled exceptions as test errors, but this
attribute allows you to change that behavior.
Note
This attribute was introduced in NUnit 4.6 to address scenarios where background thread exceptions, particularly
ThreadAbortException from Thread.Abort() calls, would cause tests to be incorrectly marked as cancelled.
Warning
This attribute only affects exceptions raised on threads other than the main test thread. Exceptions on the main test thread will always cause the test to fail, regardless of this attribute's setting.
Handling Modes
The attribute accepts an UnhandledExceptionHandling enum value:
| Mode | Description |
|---|---|
Error |
Unhandled exceptions cause the test to fail (default behavior) |
Ignore |
Unhandled exceptions are ignored and do not affect the test result |
Default |
Same as Error |
Basic Usage
Error Mode (Default)
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Error)]
public void TestWithErrorHandling()
{
// Any unhandled exception on background threads will cause this test to fail
// This is the default behavior
var task = Task.Run(() =>
{
// Work that completes successfully
Thread.Sleep(10);
});
task.Wait();
Assert.Pass();
}
Ignore Mode
Use with caution - ignoring exceptions may hide real issues:
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
public void TestIgnoringBackgroundExceptions()
{
// Background exceptions are ignored - use with caution!
// This test will pass even if background threads throw
using var cts = new CancellationTokenSource();
var task = Task.Run(() =>
{
// Simulate work
Thread.Sleep(10);
}, cts.Token);
task.Wait();
Assert.Pass();
}
Filtering by Exception Type
You can specify which exception types should be handled differently:
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(OperationCanceledException))]
public void TestIgnoringSpecificExceptions()
{
// Only OperationCanceledException is ignored on background threads
// Other exception types will still cause the test to fail
_ = Task.Run(() =>
{
// This will throw on a background thread
Thread.Sleep(10);
throw new OperationCanceledException();
});
// The unhandled OperationCanceledException on the background thread
// will be ignored by the attribute, so this test still passes
Assert.Pass();
}
When exception types are specified:
- Only the specified exception types are affected by the handling mode
- Other exception types follow the default behavior (Error)
- Multiple exception types can be specified
Fixture-Level Application
Apply to an entire fixture to affect all tests within it:
[TestFixture]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
public class LegacyCodeTests
{
// All tests in this fixture will ignore unhandled exceptions
// on background threads
[Test]
public void TestLegacyComponent()
{
// Legacy code that might throw on background threads
Assert.Pass();
}
[Test]
public void TestAnotherLegacyComponent()
{
// This test also ignores background exceptions
Assert.Pass();
}
}
Common Scenarios
Handling Thread.Abort Scenarios
When testing code that uses Thread.Abort(), the ThreadAbortException thrown on the background thread can cause NUnit
to mark the test as "cancelled by user" even though the test completed successfully. This attribute allows you to
ignore ThreadAbortException specifically:
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(ThreadAbortException))]
public void TestWithThreadAbort()
{
// Thread.Abort() is not supported on .NET 5+
// This example demonstrates the pattern for .NET Framework
if (!IsThreadAbortSupported())
{
Assert.Ignore("Thread.Abort() is not supported on this platform");
}
// This test uses Thread.Abort() which throws ThreadAbortException
// Without this attribute, the test would be marked as "cancelled by user"
var thread = new Thread(() => Thread.Sleep(1000));
thread.Start();
thread.Join(500);
#pragma warning disable SYSLIB0006 // Thread.Abort is obsolete
thread.Abort();
#pragma warning restore SYSLIB0006
Assert.Pass();
}
private static bool IsThreadAbortSupported()
{
#if NETFRAMEWORK
return true;
#else
return false;
#endif
}
Note
Thread.Abort() is obsolete in .NET 5+ and throws PlatformNotSupportedException on those platforms.
Consider migrating to CancellationToken-based cancellation patterns instead.
Testing Fire-and-Forget Operations
When testing code that intentionally spawns background work that might fail independently:
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
public void TestFireAndForgetLogging()
{
// The system under test fires off logging that we don't want to wait for
var sut = new ServiceWithBackgroundLogging();
var result = sut.DoWork();
Assert.That(result, Is.EqualTo(expected));
// Background logging exceptions won't fail this test
}
Testing Cancellation Scenarios
When OperationCanceledException on background threads is expected and should be ignored:
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(OperationCanceledException))]
public async Task TestCancellationInBackgroundWork()
{
using var cts = new CancellationTokenSource();
// Fire-and-forget background work that observes the cancellation token
_ = Task.Run(() => LongRunningOperationAsync(cts.Token));
// Trigger cancellation while the test continues
cts.Cancel();
// Perform other assertions that do not await the background work
await Task.Delay(50);
Assert.That(SomeResult(), Is.EqualTo(expected));
// Any OperationCanceledException thrown on the background task
// will be ignored by NUnit due to the UnhandledExceptionHandling setting
}
Legacy Code Integration
When integrating with legacy code that has known background thread issues:
[TestFixture]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
public class LegacySystemTests
{
// Legacy system has known issues with background thread cleanup
// that we accept for now
}
Inheritance and Scope
The attribute can be applied at multiple levels:
| Level | Scope |
|---|---|
| Assembly | Affects all tests in the assembly |
| Class | Affects all tests in the fixture |
| Method | Affects only the specific test |
When multiple levels specify the attribute, they are combined - each level's configuration is additive.
Best Practices
Warning
Using Ignore mode can hide real bugs in your code. Use it sparingly and only when you understand the implications.
- Prefer
Errormode - The default behavior ensures you're aware of all exceptions - Be specific with exception types - When using
Ignore, specify only the exception types you expect - Document why - Add comments explaining why certain exceptions are being ignored
- Review regularly - Periodically review uses of
Ignoremode to ensure they're still necessary