16 questions found'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
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
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
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
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
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