Real-world examples of unit test failure due to improper exception handling
Real examples of unit test failure due to improper exception handling
Let’s start with what you came for: concrete, realistic examples of unit test failure due to improper exception handling. These scenarios pop up across languages and frameworks, and they all share the same root problem: the code and the test disagree about how and when errors should surface.
Example 1: Swallowed exception hides a real failure
A very common example of unit test failure due to improper exception handling is the classic swallow-and-log pattern. Imagine a Java service method:
public User getUser(String id) {
try {
return userRepository.findById(id);
} catch (DatabaseException e) {
logger.error("Failed to fetch user", e);
return null; // swallow the exception
}
}
And a unit test that expects an exception:
@Test
void getUser_throwsOnDbError() {
when(userRepository.findById("123"))
.thenThrow(new DatabaseException("DB down"));
assertThrows(DatabaseException.class,
() -> userService.getUser("123"));
}
The test fails with something like:
Expected DatabaseException to be thrown, but nothing was thrown.
This is a textbook example of unit test failure due to improper exception handling: the production code eats the exception and returns null, while the test expects the error to bubble up. The fix is not just to “make the test pass,” but to decide intentionally:
- Should this method propagate the exception?
- Or should it return a fallback value and document that behavior clearly?
If the method is part of a domain service, swallowing the exception is usually a design smell. Your tests are telling you that your error propagation strategy is inconsistent.
Example 2: Asserting the wrong exception type
Another frequent example of unit test failure due to improper exception handling: the code throws one exception type, the test expects another.
Consider a Python function:
def parse_age(value: str) -> int:
try:
return int(value)
except ValueError as e:
raise RuntimeError("Invalid age") from e
And a test:
import pytest
def test_parse_age_raises_value_error_for_invalid_input():
with pytest.raises(ValueError):
parse_age("not-a-number")
The test fails:
Failed: DID NOT RAISE <class 'ValueError'>
E RuntimeError: Invalid age
Here, the developer refactored the function to wrap low-level exceptions, but the test still expects the original ValueError. This is a simple, but very real example of unit test failure due to improper exception handling.
Two takeaways:
- If you wrap exceptions, stabilize the public contract: document that
RuntimeError(or a custom domain exception) is what callers should handle. - If you want to preserve the original type for callers, you probably shouldn’t wrap it at all, or you should expose the original as part of a structured error object.
Example 3: Overly broad catch blocks causing false positives
Overly broad catch blocks create some of the best examples of unit test failure due to improper exception handling because they tend to mask unrelated bugs.
C# example:
public decimal CalculateDiscount(Order order)
{
try
{
return _discountEngine.GetDiscount(order);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to calculate discount");
return 0m; // fallback
}
}
Unit test:
[Fact]
public void CalculateDiscount_ReturnsZero_WhenEngineThrowsBusinessException()
{
_discountEngine
.Setup(x => x.GetDiscount(It.IsAny<Order>()))
.Throws(new BusinessRuleException("No discount"));
var result = _service.CalculateDiscount(new Order());
Assert.Equal(0m, result);
}
The test passes. But later, after a refactor, _discountEngine.GetDiscount throws a NullReferenceException due to a bug. The method still returns 0m, the test still passes, and your CI stays green.
This isn’t just an example of unit test failure; it’s an example of the absence of failure due to improper exception handling. Because the catch block is too broad, the test cannot distinguish between an expected business exception and a programming error.
A better pattern is to catch specific exceptions:
catch (BusinessRuleException ex)
{
_logger.LogWarning(ex, "No discount applicable");
return 0m;
}
Now, a NullReferenceException will cause the test (and production) to fail loudly, which is what you want.
Example 4: Async exceptions never observed in tests
Async code multiplies the number of examples of unit test failure due to improper exception handling.
Consider a Node.js/TypeScript service:
async function sendWelcomeEmail(user: User): Promise<void> {
try {
await emailClient.send({
to: user.email,
subject: "Welcome",
body: "Hello!",
});
} catch (err) {
console.error("Failed to send welcome email", err);
// no rethrow
}
}
Test:
it("fails when email sending fails", async () => {
jest.spyOn(emailClient, "send").mockRejectedValue(new Error("SMTP down"));
await expect(sendWelcomeEmail(user))
.rejects
.toThrow("SMTP down");
});
The test fails:
Received promise resolved instead of rejected
The async function catches the error and never rethrows, so the promise resolves successfully. This is a clear example of unit test failure due to improper exception handling in async flows.
You need to decide the contract:
- If email sending is critical, rethrow or return a
Result-style object indicating failure. - If it’s best-effort, change the test to assert that failures are logged or monitored, not that the function rejects.
Example 5: Testing framework timeouts hiding exceptions
Modern CI data from large projects (GitHub Actions, Azure DevOps, GitLab CI) shows a steady increase in test failures related to async timeouts as more teams adopt async I/O and microservices between 2022–2024. A subtle example of unit test failure due to improper exception handling is when exceptions are thrown, but never reach the test assertion because of timeouts.
Example in Jest:
it("throws when processing fails", async () => {
await processJob();
// expect something to throw inside processJob
});
And inside processJob:
export async function processJob() {
queue.consume(async (msg) => {
try {
await handleMessage(msg);
} catch (err) {
console.error("Job failed", err);
// no rethrow, no propagation
}
});
}
The test hangs or times out instead of failing with a meaningful error. The callback swallows the exception, the outer promise never rejects, and the test framework eventually kills the test.
This is another real example of unit test failure due to improper exception handling: the exception is trapped inside a callback. To make this testable, you might:
- Return a
Promisethat rejects whenhandleMessagefails. - Emit a domain event on failure and assert against that.
Example 6: Incorrect use of try/catch in parameterized tests
Parameterized tests give us more real examples of unit test failure due to improper exception handling, especially when they mix valid and invalid inputs.
JUnit example:
@ParameterizedTest
@ValueSource(strings = {"25", "-1", "abc"})
void parseAge_handlesInvalidInputs(String input) {
try {
Age age = Age.parse(input);
assertNotNull(age);
} catch (IllegalArgumentException ex) {
// expected for invalid inputs
}
}
This test is misleading. For valid input ("25"), it asserts age is not null. For invalid inputs, it silently ignores the exception. If a future change makes Age.parse("25") throw an exception, the test will still pass because the catch block treats all exceptions as expected.
This is a subtle example of unit test failure due to improper exception handling inside the test itself. Better patterns:
- Split the test into two cases: valid inputs vs invalid inputs.
- Use
assertThrowsfor invalid cases instead of catching exceptions manually.
Example 7: Mapping external API errors to the wrong domain exception
In API-heavy systems, you get some of the best examples of unit test failure due to improper exception handling when mapping low-level HTTP or SDK errors to domain-level exceptions.
Imagine a payment service in Java:
public PaymentResult charge(ChargeRequest request) {
try {
gateway.charge(request);
return PaymentResult.success();
} catch (GatewayTimeoutException e) {
throw new PaymentDeclinedException("Timeout", e);
}
}
Unit test:
@Test
void charge_throwsTimeoutException_onGatewayTimeout() {
when(gateway.charge(any()))
.thenThrow(new GatewayTimeoutException("504"));
assertThrows(GatewayTimeoutException.class,
() -> paymentService.charge(request));
}
The test fails because the service throws PaymentDeclinedException, not GatewayTimeoutException. This is an instructive example of unit test failure due to improper exception handling at boundaries: the domain layer intentionally hides infrastructure-specific exceptions.
The fix is to align expectations:
- Either change the service to propagate
GatewayTimeoutExceptiondirectly, or - Update the test to expect
PaymentDeclinedExceptionand possibly inspect the cause.
Example 8: Misconfigured test doubles that never throw
Sometimes the failure isn’t in the production code, but in how the test simulates exceptions.
A Jest example:
it("logs and rethrows when DB fails", async () => {
const db = { save: jest.fn() };
db.save.mockImplementation(() => { new Error("DB down"); });
await expect(service.saveUser(user)).rejects.toThrow("DB down");
});
The test fails with:
Received promise resolved instead of rejected
The mock implementation creates an error but never throws it or returns a rejected promise. The service never sees an error, so the test asserts a failure that never happens.
This is a practical example of unit test failure due to improper exception handling inside the test setup. The mock should be:
db.save.mockRejectedValue(new Error("DB down"));
or:
db.save.mockImplementation(() => { throw new Error("DB down"); });
It’s still an exception-handling problem, just on the test side of the fence.
Patterns behind these examples of unit test failure
Across these real examples of unit test failure due to improper exception handling, a few patterns keep showing up:
- Swallowing vs propagating: Methods that log and return
nullor a default value often surprise tests that expect an exception. - Type mismatch: Wrapping or changing exception types without updating tests leads to failures that look like “expected X, got Y.”
- Scope mismatch: Exceptions thrown in callbacks, async tasks, or background workers never reach the test framework unless explicitly surfaced.
- Over-broad catching:
catch (Exception)orcatch (Throwable)hides genuine bugs and makes tests pass when they should fail. - Tests that over-catch: Tests that use
try/catchinstead of dedicated assertion helpers often mask failures. - Bad mocks: Misconfigured test doubles that don’t actually throw or reject create misleading unit test failures.
These patterns align with long-standing software engineering guidance around error handling. For example, Carnegie Mellon’s SEI has repeatedly emphasized precise error handling and clear failure modes in its secure coding guidelines (SEI CERT Java Coding Guidelines). While that guidance is security-focused, the same principles apply directly to writing reliable unit tests around exceptions.
How to design tests that expose improper exception handling
Using the above examples of unit test failure due to improper exception handling as a guide, you can tune your test design to catch these issues earlier:
- Assert on behavior, not just type: Don’t only assert that an exception is thrown; assert on the message, error code, or side effects where it makes sense.
- Test both happy and unhappy paths: For every method that can fail, write at least one test that expects success and one that expects failure.
- Avoid generic catch in tests: Use your framework’s
assertThrows,pytest.raises, orassertRaiseshelpers instead of manualtry/catch. - Expose async failures: Make async APIs return promises or tasks that reflect failure states instead of burying exceptions in callbacks.
- Limit catch scope: In production code, catch the narrowest exception type you can, and let everything else fail fast.
If you’re interested in broader software testing practice, resources like NIST’s software assurance materials and university testing courses (for example, MIT’s open courseware on software engineering at mit.edu) reinforce the value of explicit failure behavior and clear exception contracts.
FAQ: common questions about exception-related test failures
Q1. What is a simple example of unit test failure due to improper exception handling?
A straightforward example is a method that catches an exception, logs it, and returns null, while the test expects that exception to be thrown. The test fails with “expected exception X, but nothing was thrown,” even though the underlying operation did fail.
Q2. How can I avoid these examples of unit test failure in async code?
Design async functions so they return a promise or task that reflects failure states. Avoid burying exceptions in callbacks or event handlers. In tests, always await async calls and use your framework’s async assertion helpers (rejects.toThrow, async assertThrows, etc.).
Q3. Are broad catch blocks always bad for unit tests?
They’re not always wrong, but they often lead to the worst examples of unit test failure due to improper exception handling. If you must use a broad catch for logging or cleanup, rethrow or wrap the exception so the test framework still sees a failure for programming errors.
Q4. Should I test the exact exception type or just that an error occurred?
It depends on the contract. For public APIs and domain services, testing the exact type (or a base type) is usually appropriate. For internal helper methods, asserting that “some error” occurred may be enough. The important part is that your tests match the documented behavior of the method.
Q5. How do I handle legacy code that swallows exceptions everywhere?
Start by adding characterization tests that document current behavior, even if it’s not ideal. Then, incrementally refactor: narrow catch blocks, rethrow unexpected errors, and introduce domain-specific exceptions. Each change should come with updated tests so you don’t trade one set of brittle behaviors for another.
By studying these real examples of unit test failure due to improper exception handling, you can turn flaky, misleading tests into a tighter feedback loop that actually reflects how your system behaves under stress—exactly what you want from a test suite in 2024 and beyond.
Related Topics
Real-world examples of unit test failure due to race conditions in modern codebases
Best examples of unit test failure examples: incorrect test setup
Real-world examples of unit test failures from outdated libraries
Real-world examples of unit test failure due to improper exception handling
Real-world examples of unit test failures: mocking issues examples for modern codebases
Best examples of unit test failure examples: test doubles in real projects
Explore More Unit Testing Failures
Discover more examples and insights in this category.
View All Unit Testing Failures