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.
Constructors
UnhandledExceptionHandlingAttribute(UnhandledExceptionHandling handling)
UnhandledExceptionHandlingAttribute(UnhandledExceptionHandling handling, params Type[] exceptions)
| Parameter | Type | Description |
|---|---|---|
handling |
UnhandledExceptionHandling |
How NUnit treats a matching unhandled exception. |
exceptions |
Type[] |
Exception types this entry applies to. When omitted, the entry applies to all exception types. |
The attribute may be applied more than once to the same element, so that different exception types get different handling.
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 |
Applies To
| Test Methods | Test Fixtures (Classes) | Assembly |
|---|---|---|
| ✅ | ✅ | ✅ |
Platform Support
What this attribute can do depends on what the runtime allows.
- .NET Framework: an unhandled exception on another thread is reported to NUnit, which attributes it to the test
that was running and either records an error or, with
Ignore, lets the test complete normally. This is the platform the attribute was designed for, and the only one whereIgnorekeeps the process alive. - .NET Core and .NET 5 and later: an unhandled exception on a thread terminates the test process, whatever this
attribute says. NUnit still receives the exception first and records an error when the mode is
Error, butIgnorecannot prevent the crash. .NET 10 added an API that lets a handler mark such an exception as handled, but NUnit does not use it yet. - Unobserved
Taskexceptions: these do not terminate the process on modern .NET. They are reported on the finalizer thread, where NUnit usually cannot tell which test started the task, so they are logged and do not fail any test, regardless of this attribute.
In practice the attribute matters for exceptions thrown on threads your test code creates itself, such as new Thread,
and for ThreadAbortException on .NET Framework.
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()
{
// An exception escaping this thread is not recorded against the test.
// On .NET Framework the test then passes; on .NET Core and later the
// runtime still terminates the process, so the thread procedure below
// must not actually throw.
var thread = new Thread(() => Thread.Sleep(10));
thread.Start();
thread.Join();
Assert.Pass();
}
Filtering by Exception Type
You can specify which exception types should be handled differently:
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(ObjectDisposedException))]
public void TestIgnoringSpecificExceptions()
{
// Only ObjectDisposedException escaping a background thread is ignored.
// Any other exception type is still recorded as a test error.
var thread = new Thread(() => Thread.Sleep(10));
thread.Start();
thread.Join();
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.
Background Threads Started by the Code Under Test (.NET Framework)
When the system under test starts its own threads and a failure on one of them is a known, accepted condition:
[TestFixture]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore, typeof(ObjectDisposedException))]
public class LegacySystemTests
{
// The legacy component's cleanup thread sometimes throws ObjectDisposedException
// after the test has finished with it. On .NET Framework that no longer fails the test.
}
On .NET Core and later the same exception would terminate the process, so the only real fix there is to stop the exception from escaping the thread, for example by catching it in the thread procedure.
Precedence When Applied at Several Levels
The attribute can be applied at the assembly, fixture and method level. When an unhandled exception arrives, NUnit looks for a matching entry starting at the test method, then the fixture, then the assembly, and uses the first level that has one:
- At a given level, an entry that names the exception type wins over an entry that covers all exception types.
- If a level has no entry for that exception, NUnit moves up to the next level.
- If no level matches, the default
Errorapplies.
Settings are not merged across levels. A method-level [UnhandledExceptionHandling(Ignore)] covers every exception
type for that test, so a fixture-level entry for a specific type is never consulted for it. To ignore everything except
one type at the same level, give that type its own Error entry:
[Test]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Ignore)]
[UnhandledExceptionHandling(UnhandledExceptionHandling.Error, typeof(InvalidOperationException))]
public void IgnoresEverythingExceptInvalidOperation()
{
// ...
}
When to Use It
Warning
Using Ignore mode can hide real bugs in your code. Use it sparingly and only when you understand the implications.
The attribute is appropriate when:
- You are on .NET Framework and test code that uses
Thread.Abort(), soThreadAbortExceptionis expected. - The code under test runs a background thread whose failure is already known, cannot be fixed in the test, and is unrelated to what the test verifies. Name the exception type rather than ignoring everything.
It is not a substitute for awaiting tasks or joining threads before the test ends. If the test can observe the background work, do that instead, so a failure is reported where it happened.
- 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