16 questions found
Why does 'throw;' (bare throw) preserve the original stack trace, while 'throw ex;' does not?
Intermediate
'throw;' re-throws the CURRENT exception exactly as caught, preserving its original stack trace all the way back to where it was FIRST thrown; 'throw ex;' treats it as throwing a NEW exception from THIS point, overwriting the stack trace and losing the original throw location — making the bare 'throw;' the correct choice when re-throwing after logging or partial handling.
try {
DoSomethingRisky();
} catch (Exception ex) {
LogError(ex);
throw; // CORRECT: preserves the original stack trace
// throw ex; // WRONG: resets the stack trace to this line
}
Real-world example
Logging an exception's details before re-throwing it, without losing the diagnostic value of the original stack trace.
Common follow-ups: Is there any legitimate reason you'd deliberately want to use 'throw ex;' instead of bare 'throw;'?
Fundamentals
How does exception handling interact with async/await, and how are exceptions from an async Task propagated to the awaiting caller?
Advanced
An exception thrown inside an async method is captured and stored WITHIN the returned Task object (rather than thrown immediately at the call site); it's only actually re-thrown when the caller AWAITS that Task — this is why unobserved (never-awaited) Task exceptions can silently vanish, and why try/catch around an 'await' correctly catches exceptions from the awaited operation.
async Task<int> RiskyOperationAsync() {
throw new InvalidOperationException("Failed");
}
try {
int result = await RiskyOperationAsync(); // exception surfaces HERE, at the await
} catch (InvalidOperationException ex) {
Console.WriteLine($"Caught: {ex.Message}");
}
Real-world example
Understanding why an exception from a 'fire-and-forget' unwaited Task might silently disappear instead of crashing anything visibly.
Common follow-ups: What is an 'unobserved task exception,' and how did its default behavior change between older and newer .NET versions?
Asynchronous Programming
How does AggregateException work with Task.WhenAll() when multiple concurrent tasks throw exceptions?
Advanced
Task.WhenAll() wraps ALL exceptions from ALL failed tasks into a SINGLE AggregateException, accessible via its .InnerExceptions collection; however, when you 'await' the WhenAll() result directly, only the FIRST exception is actually re-thrown to you (as its own specific type) — you must inspect the ORIGINAL Task objects individually if you need every failure's details.
var task1 = Task.Run(() => throw new InvalidOperationException("Error 1"));
var task2 = Task.Run(() => throw new ArgumentException("Error 2"));
try {
await Task.WhenAll(task1, task2); // only the FIRST exception surfaces here
} catch (Exception ex) {
// To see ALL errors, check task1.Exception and task2.Exception individually
}
Real-world example
Collecting and reporting EVERY failure from a batch of concurrent operations, not just the first one encountered.
Common follow-ups: How would you access the FULL AggregateException with all inner exceptions, rather than just the first one that 'await' surfaces?
Multithreading & Task Parallel Library
What is 'exception filtering with side effects' and why is it considered a controversial but sometimes useful technique for logging?
Advanced
Since a 'when' filter's condition is evaluated BEFORE deciding whether to catch, you can put a logging call inside the filter expression itself (returning true always) to log EVERY exception as it passes through — even ones that ultimately AREN'T caught by this particular block — though many consider embedding side effects in a filter condition a code smell, since it obscures the filter's true purpose.
try {
DoWork();
} catch (Exception ex) when (LogAndReturnFalse(ex)) {
// never actually reached, since LogAndReturnFalse always returns false
}
bool LogAndReturnFalse(Exception ex) {
Console.WriteLine($"Observed (but not handled): {ex.Message}");
return false; // lets the exception continue propagating after logging
}
Real-world example
Globally logging every exception that passes through a certain layer of code, even ones ultimately handled elsewhere further up the call stack.
Common follow-ups: What's a cleaner, less surprising alternative to achieve the same 'log everything, handle selectively' goal?
Asynchronous Programming
How would you design a robust global exception handling middleware in ASP.NET Core to convert unhandled exceptions into consistent API error responses?
Advanced
Register custom exception-handling middleware (or use the built-in UseExceptionHandler) early in the pipeline to catch ANY unhandled exception from later middleware/controllers, log it, and transform it into a consistent, structured error response (like the RFC 7807 Problem Details format) instead of leaking raw stack traces to API consumers.
app.UseExceptionHandler(errorApp => {
errorApp.Run(async context => {
var exceptionFeature = context.Features.Get<IExceptionHandlerFeature>();
context.Response.StatusCode = 500;
context.Response.ContentType = "application/problem+json";
await context.Response.WriteAsJsonAsync(new {
title = "An unexpected error occurred",
status = 500,
detail = app.Environment.IsDevelopment() ? exceptionFeature?.Error.Message : null
});
});
});
Real-world example
Ensuring a production API never leaks internal stack traces or exception details to external clients, while still logging full details server-side.
Common follow-ups: Why is it important to conditionally hide exception details based on the environment (Development vs Production)?
Fundamentals
How does exception performance (throwing/catching) compare to normal control flow, and why should exceptions generally NOT be used for expected, routine control flow?
Advanced
Throwing an exception involves capturing a full stack trace and unwinding the call stack, which is SIGNIFICANTLY more expensive (often orders of magnitude slower) than a normal return value or conditional check — exceptions should be reserved for genuinely EXCEPTIONAL, unexpected conditions, not routine logic like 'no results found,' which should instead use return values (like a nullable result, or the Result<T,E> pattern).
// Expensive anti-pattern: using exceptions for expected, routine 'not found' logic
try {
var user = repository.GetByIdOrThrow(id); // throws routinely when not found
} catch (NotFoundException) { return null; }
// Better: use a return value for an expected, routine outcome
var user = repository.TryGetById(id); // returns null, no exception overhead
Real-world example
Refactoring a hot-path lookup method that routinely 'fails' (like a cache miss) to avoid the real performance cost of exception-based control flow.
Common follow-ups: Roughly how much slower is throwing/catching an exception compared to a simple conditional check, in relative terms?
Memory & Garbage Collection