[{"data":1,"prerenderedAt":1476},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-exception-handling-best-practices":3},{"id":4,"title":5,"body":6,"description":1461,"difficulty":1462,"extension":1463,"framework":1464,"frameworkSlug":1465,"meta":1466,"navigation":95,"order":99,"path":1467,"qaPath":1468,"seo":1469,"stem":1470,"subtopic":1471,"topic":1472,"topicSlug":1473,"updated":1474,"__hash__":1475},"blog\u002Fblog\u002Fdotnet-exception-handling-best-practices.md","Exception Handling Patterns in C#",{"type":7,"value":8,"toc":1443},"minimark",[9,14,27,31,41,59,63,176,189,193,287,300,304,307,454,457,490,494,508,686,690,701,794,804,808,888,902,906,911,1080,1084,1140,1144,1224,1228,1316,1320,1396,1406,1410,1439],[10,11,13],"h2",{"id":12},"why-exception-handling-is-a-code-quality-signal","Why exception handling is a code quality signal",[15,16,17,18,22,23,26],"p",{},"How a developer handles exceptions reveals a lot about their maturity. Swallowing\nexceptions, using ",[19,20,21],"code",{},"throw ex"," (losing the stack trace), catching ",[19,24,25],{},"Exception"," without\nlogging — these mistakes make debugging production incidents dramatically harder.\nInterviews probe exception handling because it's a window into a candidate's reliability\ninstincts.",[10,28,30],{"id":29},"the-exception-hierarchy-what-to-catch-and-what-not-to","The exception hierarchy — what to catch and what not to",[32,33,38],"pre",{"className":34,"code":36,"language":37},[35],"language-text","Exception\n├── SystemException           (runtime — often unrecoverable)\n│   ├── NullReferenceException\n│   ├── InvalidOperationException\n│   ├── ArgumentException\n│   │   ├── ArgumentNullException\n│   │   └── ArgumentOutOfRangeException\n│   ├── IOException\n│   │   └── FileNotFoundException\n│   ├── OutOfMemoryException  ← do NOT catch\n│   └── StackOverflowException ← process terminates; unreachable\n└── ApplicationException      ← deprecated; avoid as base class\n","text",[19,39,36],{"__ignoreMap":40},"",[15,42,43,47,48,50,51,54,55,58],{},[44,45,46],"strong",{},"Rule:"," Catch the most specific type you can handle. Only catch ",[19,49,25],{}," as a\nlast resort — log and re-throw. Never catch ",[19,52,53],{},"OutOfMemoryException"," or\n",[19,56,57],{},"StackOverflowException",".",[10,60,62],{"id":61},"throw-vs-throw-ex-the-most-common-mistake","throw vs throw ex — the most common mistake",[32,64,68],{"className":65,"code":66,"language":67,"meta":40,"style":40},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F throw ex — LOSES the original stack trace:\ntry { Inner(); }\ncatch (Exception ex) { throw ex; } \u002F\u002F stack trace now points to this catch block!\n\n\u002F\u002F throw — PRESERVES the original stack trace:\ntry { Inner(); }\ncatch (Exception ex)\n{\n    logger.LogError(ex, \"Inner failed\");\n    throw; \u002F\u002F re-throws the same exception with original stack trace intact\n}\n\n\u002F\u002F Wrapping — preserve original as InnerException:\ntry { Inner(); }\ncatch (DatabaseException ex)\n{\n    throw new RepositoryException(\"Failed to save order\", ex); \u002F\u002F original in InnerException\n}\n","csharp",[19,69,70,78,84,90,97,103,108,114,120,126,132,138,143,149,154,160,165,171],{"__ignoreMap":40},[71,72,75],"span",{"class":73,"line":74},"line",1,[71,76,77],{},"\u002F\u002F throw ex — LOSES the original stack trace:\n",[71,79,81],{"class":73,"line":80},2,[71,82,83],{},"try { Inner(); }\n",[71,85,87],{"class":73,"line":86},3,[71,88,89],{},"catch (Exception ex) { throw ex; } \u002F\u002F stack trace now points to this catch block!\n",[71,91,93],{"class":73,"line":92},4,[71,94,96],{"emptyLinePlaceholder":95},true,"\n",[71,98,100],{"class":73,"line":99},5,[71,101,102],{},"\u002F\u002F throw — PRESERVES the original stack trace:\n",[71,104,106],{"class":73,"line":105},6,[71,107,83],{},[71,109,111],{"class":73,"line":110},7,[71,112,113],{},"catch (Exception ex)\n",[71,115,117],{"class":73,"line":116},8,[71,118,119],{},"{\n",[71,121,123],{"class":73,"line":122},9,[71,124,125],{},"    logger.LogError(ex, \"Inner failed\");\n",[71,127,129],{"class":73,"line":128},10,[71,130,131],{},"    throw; \u002F\u002F re-throws the same exception with original stack trace intact\n",[71,133,135],{"class":73,"line":134},11,[71,136,137],{},"}\n",[71,139,141],{"class":73,"line":140},12,[71,142,96],{"emptyLinePlaceholder":95},[71,144,146],{"class":73,"line":145},13,[71,147,148],{},"\u002F\u002F Wrapping — preserve original as InnerException:\n",[71,150,152],{"class":73,"line":151},14,[71,153,83],{},[71,155,157],{"class":73,"line":156},15,[71,158,159],{},"catch (DatabaseException ex)\n",[71,161,163],{"class":73,"line":162},16,[71,164,119],{},[71,166,168],{"class":73,"line":167},17,[71,169,170],{},"    throw new RepositoryException(\"Failed to save order\", ex); \u002F\u002F original in InnerException\n",[71,172,174],{"class":73,"line":173},18,[71,175,137],{},[15,177,178,181,182,184,185,188],{},[44,179,180],{},"When debugging a production incident, the stack trace is your map."," ",[19,183,21],{}," tears\nthat map in half. Use bare ",[19,186,187],{},"throw"," unless you have a specific reason to wrap.",[10,190,192],{"id":191},"finally-cleanup-that-always-runs","finally — cleanup that always runs",[32,194,196],{"className":65,"code":195,"language":67,"meta":40,"style":40},"FileStream? fs = null;\ntry\n{\n    fs = File.OpenRead(\"data.txt\");\n    return Process(fs);\n}\ncatch (IOException ex)\n{\n    logger.LogError(ex, \"File read failed\");\n    throw;\n}\nfinally\n{\n    fs?.Close(); \u002F\u002F always runs: success, exception, or return\n}\n\n\u002F\u002F Modern equivalent — prefer 'using' for IDisposable:\nusing var stream = File.OpenRead(\"data.txt\"); \u002F\u002F Dispose called automatically\nreturn Process(stream); \u002F\u002F even if Process throws, stream is disposed\n",[19,197,198,203,208,212,217,222,226,231,235,240,245,249,254,258,263,267,271,276,281],{"__ignoreMap":40},[71,199,200],{"class":73,"line":74},[71,201,202],{},"FileStream? fs = null;\n",[71,204,205],{"class":73,"line":80},[71,206,207],{},"try\n",[71,209,210],{"class":73,"line":86},[71,211,119],{},[71,213,214],{"class":73,"line":92},[71,215,216],{},"    fs = File.OpenRead(\"data.txt\");\n",[71,218,219],{"class":73,"line":99},[71,220,221],{},"    return Process(fs);\n",[71,223,224],{"class":73,"line":105},[71,225,137],{},[71,227,228],{"class":73,"line":110},[71,229,230],{},"catch (IOException ex)\n",[71,232,233],{"class":73,"line":116},[71,234,119],{},[71,236,237],{"class":73,"line":122},[71,238,239],{},"    logger.LogError(ex, \"File read failed\");\n",[71,241,242],{"class":73,"line":128},[71,243,244],{},"    throw;\n",[71,246,247],{"class":73,"line":134},[71,248,137],{},[71,250,251],{"class":73,"line":140},[71,252,253],{},"finally\n",[71,255,256],{"class":73,"line":145},[71,257,119],{},[71,259,260],{"class":73,"line":151},[71,261,262],{},"    fs?.Close(); \u002F\u002F always runs: success, exception, or return\n",[71,264,265],{"class":73,"line":156},[71,266,137],{},[71,268,269],{"class":73,"line":162},[71,270,96],{"emptyLinePlaceholder":95},[71,272,273],{"class":73,"line":167},[71,274,275],{},"\u002F\u002F Modern equivalent — prefer 'using' for IDisposable:\n",[71,277,278],{"class":73,"line":173},[71,279,280],{},"using var stream = File.OpenRead(\"data.txt\"); \u002F\u002F Dispose called automatically\n",[71,282,284],{"class":73,"line":283},19,[71,285,286],{},"return Process(stream); \u002F\u002F even if Process throws, stream is disposed\n",[15,288,289,292,293,295,296,299],{},[19,290,291],{},"finally"," does not run for ",[19,294,57],{}," or ",[19,297,298],{},"Environment.FailFast()"," — the\nprocess terminates immediately in both cases.",[10,301,303],{"id":302},"custom-exceptions-when-and-how","Custom exceptions — when and how",[15,305,306],{},"Create a custom exception when callers need to catch your specific error programmatically,\nseparate from other errors. Don't create one just for a custom message.",[32,308,310],{"className":65,"code":309,"language":67,"meta":40,"style":40},"\u002F\u002F Well-designed custom exception:\npublic class PaymentDeclinedException : Exception\n{\n    public string DeclineCode { get; }\n    public string LastFour    { get; }\n\n    \u002F\u002F Standard constructors required for serialisation + chaining:\n    public PaymentDeclinedException() { }\n    public PaymentDeclinedException(string message) : base(message) { }\n    public PaymentDeclinedException(string message, Exception inner)\n        : base(message, inner) { }\n\n    \u002F\u002F Domain-specific constructor:\n    public PaymentDeclinedException(string declineCode, string lastFour)\n        : base($\"Payment declined ({declineCode}) for card ending {lastFour}\")\n    {\n        DeclineCode = declineCode;\n        LastFour    = lastFour;\n    }\n}\n\n\u002F\u002F Catch specifically:\ntry { await ProcessPaymentAsync(card, amount); }\ncatch (PaymentDeclinedException ex)\n{\n    \u002F\u002F Code and last four digits are available — no message parsing needed\n    await NotifyUserAsync(ex.DeclineCode, ex.LastFour);\n}\n",[19,311,312,317,322,326,331,336,340,345,350,355,360,365,369,374,379,384,389,394,399,404,409,414,420,426,432,437,443,449],{"__ignoreMap":40},[71,313,314],{"class":73,"line":74},[71,315,316],{},"\u002F\u002F Well-designed custom exception:\n",[71,318,319],{"class":73,"line":80},[71,320,321],{},"public class PaymentDeclinedException : Exception\n",[71,323,324],{"class":73,"line":86},[71,325,119],{},[71,327,328],{"class":73,"line":92},[71,329,330],{},"    public string DeclineCode { get; }\n",[71,332,333],{"class":73,"line":99},[71,334,335],{},"    public string LastFour    { get; }\n",[71,337,338],{"class":73,"line":105},[71,339,96],{"emptyLinePlaceholder":95},[71,341,342],{"class":73,"line":110},[71,343,344],{},"    \u002F\u002F Standard constructors required for serialisation + chaining:\n",[71,346,347],{"class":73,"line":116},[71,348,349],{},"    public PaymentDeclinedException() { }\n",[71,351,352],{"class":73,"line":122},[71,353,354],{},"    public PaymentDeclinedException(string message) : base(message) { }\n",[71,356,357],{"class":73,"line":128},[71,358,359],{},"    public PaymentDeclinedException(string message, Exception inner)\n",[71,361,362],{"class":73,"line":134},[71,363,364],{},"        : base(message, inner) { }\n",[71,366,367],{"class":73,"line":140},[71,368,96],{"emptyLinePlaceholder":95},[71,370,371],{"class":73,"line":145},[71,372,373],{},"    \u002F\u002F Domain-specific constructor:\n",[71,375,376],{"class":73,"line":151},[71,377,378],{},"    public PaymentDeclinedException(string declineCode, string lastFour)\n",[71,380,381],{"class":73,"line":156},[71,382,383],{},"        : base($\"Payment declined ({declineCode}) for card ending {lastFour}\")\n",[71,385,386],{"class":73,"line":162},[71,387,388],{},"    {\n",[71,390,391],{"class":73,"line":167},[71,392,393],{},"        DeclineCode = declineCode;\n",[71,395,396],{"class":73,"line":173},[71,397,398],{},"        LastFour    = lastFour;\n",[71,400,401],{"class":73,"line":283},[71,402,403],{},"    }\n",[71,405,407],{"class":73,"line":406},20,[71,408,137],{},[71,410,412],{"class":73,"line":411},21,[71,413,96],{"emptyLinePlaceholder":95},[71,415,417],{"class":73,"line":416},22,[71,418,419],{},"\u002F\u002F Catch specifically:\n",[71,421,423],{"class":73,"line":422},23,[71,424,425],{},"try { await ProcessPaymentAsync(card, amount); }\n",[71,427,429],{"class":73,"line":428},24,[71,430,431],{},"catch (PaymentDeclinedException ex)\n",[71,433,435],{"class":73,"line":434},25,[71,436,119],{},[71,438,440],{"class":73,"line":439},26,[71,441,442],{},"    \u002F\u002F Code and last four digits are available — no message parsing needed\n",[71,444,446],{"class":73,"line":445},27,[71,447,448],{},"    await NotifyUserAsync(ex.DeclineCode, ex.LastFour);\n",[71,450,452],{"class":73,"line":451},28,[71,453,137],{},[15,455,456],{},"For simple input validation in .NET 6+, use the built-in throw-helpers:",[32,458,460],{"className":65,"code":459,"language":67,"meta":40,"style":40},"public void SetAge(int age)\n{\n    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(age);\n    ArgumentOutOfRangeException.ThrowIfGreaterThan(age, 150);\n    _age = age;\n}\n",[19,461,462,467,471,476,481,486],{"__ignoreMap":40},[71,463,464],{"class":73,"line":74},[71,465,466],{},"public void SetAge(int age)\n",[71,468,469],{"class":73,"line":80},[71,470,119],{},[71,472,473],{"class":73,"line":86},[71,474,475],{},"    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(age);\n",[71,477,478],{"class":73,"line":92},[71,479,480],{},"    ArgumentOutOfRangeException.ThrowIfGreaterThan(age, 150);\n",[71,482,483],{"class":73,"line":99},[71,484,485],{},"    _age = age;\n",[71,487,488],{"class":73,"line":105},[71,489,137],{},[10,491,493],{"id":492},"aggregateexception-multiple-failures-from-parallel-work","AggregateException — multiple failures from parallel work",[15,495,496,499,500,503,504,507],{},[19,497,498],{},"Task.WhenAll"," and ",[19,501,502],{},"Parallel.ForEach"," wrap multiple exceptions in ",[19,505,506],{},"AggregateException",":",[32,509,511],{"className":65,"code":510,"language":67,"meta":40,"style":40},"var t1 = ProcessAsync(item1); \u002F\u002F might throw\nvar t2 = ProcessAsync(item2); \u002F\u002F might throw\n\ntry\n{\n    await Task.WhenAll(t1, t2);\n}\ncatch\n{\n    \u002F\u002F 'await' re-throws only the FIRST inner exception\n    \u002F\u002F To inspect ALL exceptions:\n    foreach (var t in new[] { t1, t2 }.Where(t => t.IsFaulted))\n    {\n        var inner = t.Exception!.InnerException!;\n        logger.LogError(inner, \"Task failed: {Message}\", inner.Message);\n    }\n}\n\n\u002F\u002F Parallel.ForEach:\ntry\n{\n    Parallel.ForEach(items, item => ProcessItem(item));\n}\ncatch (AggregateException agg)\n{\n    \u002F\u002F Handle selectively — return true = handled; false = re-throw\n    agg.Handle(ex =>\n    {\n        if (ex is ValidationException ve)\n        {\n            logger.LogWarning(ve, \"Validation error\");\n            return true; \u002F\u002F handled — don't rethrow\n        }\n        return false; \u002F\u002F unhandled — will be re-thrown\n    });\n}\n",[19,512,513,518,523,527,531,535,540,544,549,553,558,563,568,572,577,582,586,590,594,599,603,607,612,616,621,625,630,635,639,645,651,657,663,669,675,681],{"__ignoreMap":40},[71,514,515],{"class":73,"line":74},[71,516,517],{},"var t1 = ProcessAsync(item1); \u002F\u002F might throw\n",[71,519,520],{"class":73,"line":80},[71,521,522],{},"var t2 = ProcessAsync(item2); \u002F\u002F might throw\n",[71,524,525],{"class":73,"line":86},[71,526,96],{"emptyLinePlaceholder":95},[71,528,529],{"class":73,"line":92},[71,530,207],{},[71,532,533],{"class":73,"line":99},[71,534,119],{},[71,536,537],{"class":73,"line":105},[71,538,539],{},"    await Task.WhenAll(t1, t2);\n",[71,541,542],{"class":73,"line":110},[71,543,137],{},[71,545,546],{"class":73,"line":116},[71,547,548],{},"catch\n",[71,550,551],{"class":73,"line":122},[71,552,119],{},[71,554,555],{"class":73,"line":128},[71,556,557],{},"    \u002F\u002F 'await' re-throws only the FIRST inner exception\n",[71,559,560],{"class":73,"line":134},[71,561,562],{},"    \u002F\u002F To inspect ALL exceptions:\n",[71,564,565],{"class":73,"line":140},[71,566,567],{},"    foreach (var t in new[] { t1, t2 }.Where(t => t.IsFaulted))\n",[71,569,570],{"class":73,"line":145},[71,571,388],{},[71,573,574],{"class":73,"line":151},[71,575,576],{},"        var inner = t.Exception!.InnerException!;\n",[71,578,579],{"class":73,"line":156},[71,580,581],{},"        logger.LogError(inner, \"Task failed: {Message}\", inner.Message);\n",[71,583,584],{"class":73,"line":162},[71,585,403],{},[71,587,588],{"class":73,"line":167},[71,589,137],{},[71,591,592],{"class":73,"line":173},[71,593,96],{"emptyLinePlaceholder":95},[71,595,596],{"class":73,"line":283},[71,597,598],{},"\u002F\u002F Parallel.ForEach:\n",[71,600,601],{"class":73,"line":406},[71,602,207],{},[71,604,605],{"class":73,"line":411},[71,606,119],{},[71,608,609],{"class":73,"line":416},[71,610,611],{},"    Parallel.ForEach(items, item => ProcessItem(item));\n",[71,613,614],{"class":73,"line":422},[71,615,137],{},[71,617,618],{"class":73,"line":428},[71,619,620],{},"catch (AggregateException agg)\n",[71,622,623],{"class":73,"line":434},[71,624,119],{},[71,626,627],{"class":73,"line":439},[71,628,629],{},"    \u002F\u002F Handle selectively — return true = handled; false = re-throw\n",[71,631,632],{"class":73,"line":445},[71,633,634],{},"    agg.Handle(ex =>\n",[71,636,637],{"class":73,"line":451},[71,638,388],{},[71,640,642],{"class":73,"line":641},29,[71,643,644],{},"        if (ex is ValidationException ve)\n",[71,646,648],{"class":73,"line":647},30,[71,649,650],{},"        {\n",[71,652,654],{"class":73,"line":653},31,[71,655,656],{},"            logger.LogWarning(ve, \"Validation error\");\n",[71,658,660],{"class":73,"line":659},32,[71,661,662],{},"            return true; \u002F\u002F handled — don't rethrow\n",[71,664,666],{"class":73,"line":665},33,[71,667,668],{},"        }\n",[71,670,672],{"class":73,"line":671},34,[71,673,674],{},"        return false; \u002F\u002F unhandled — will be re-thrown\n",[71,676,678],{"class":73,"line":677},35,[71,679,680],{},"    });\n",[71,682,684],{"class":73,"line":683},36,[71,685,137],{},[10,687,689],{"id":688},"exception-filters-when-clause","Exception filters (when clause)",[15,691,692,693,696,697,700],{},"A ",[19,694,695],{},"when"," clause adds a condition to a catch block without unwinding the stack if it\nevaluates to ",[19,698,699],{},"false",". This means the exception can be caught by a handler further up:",[32,702,704],{"className":65,"code":703,"language":67,"meta":40,"style":40},"\u002F\u002F Only catch specific HTTP errors:\ntry { var data = await client.GetAsync(url); data.EnsureSuccessStatusCode(); }\ncatch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)\n{\n    return null; \u002F\u002F treat as \"not found\"\n}\ncatch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ServiceUnavailable)\n{\n    throw new RetryableException(\"Service unavailable\", ex);\n}\n\u002F\u002F Other HttpRequestExceptions propagate naturally\n\n\u002F\u002F Log without catching — filter that always returns false:\ncatch (Exception ex) when (LogAndReturnFalse(ex)) { \u002F* unreachable *\u002F }\nbool LogAndReturnFalse(Exception ex)\n{\n    logger.LogError(ex, \"Unhandled exception\");\n    return false; \u002F\u002F exception continues to propagate — stack trace preserved!\n}\n",[19,705,706,711,716,721,725,730,734,739,743,748,752,757,761,766,771,776,780,785,790],{"__ignoreMap":40},[71,707,708],{"class":73,"line":74},[71,709,710],{},"\u002F\u002F Only catch specific HTTP errors:\n",[71,712,713],{"class":73,"line":80},[71,714,715],{},"try { var data = await client.GetAsync(url); data.EnsureSuccessStatusCode(); }\n",[71,717,718],{"class":73,"line":86},[71,719,720],{},"catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)\n",[71,722,723],{"class":73,"line":92},[71,724,119],{},[71,726,727],{"class":73,"line":99},[71,728,729],{},"    return null; \u002F\u002F treat as \"not found\"\n",[71,731,732],{"class":73,"line":105},[71,733,137],{},[71,735,736],{"class":73,"line":110},[71,737,738],{},"catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.ServiceUnavailable)\n",[71,740,741],{"class":73,"line":116},[71,742,119],{},[71,744,745],{"class":73,"line":122},[71,746,747],{},"    throw new RetryableException(\"Service unavailable\", ex);\n",[71,749,750],{"class":73,"line":128},[71,751,137],{},[71,753,754],{"class":73,"line":134},[71,755,756],{},"\u002F\u002F Other HttpRequestExceptions propagate naturally\n",[71,758,759],{"class":73,"line":140},[71,760,96],{"emptyLinePlaceholder":95},[71,762,763],{"class":73,"line":145},[71,764,765],{},"\u002F\u002F Log without catching — filter that always returns false:\n",[71,767,768],{"class":73,"line":151},[71,769,770],{},"catch (Exception ex) when (LogAndReturnFalse(ex)) { \u002F* unreachable *\u002F }\n",[71,772,773],{"class":73,"line":156},[71,774,775],{},"bool LogAndReturnFalse(Exception ex)\n",[71,777,778],{"class":73,"line":162},[71,779,119],{},[71,781,782],{"class":73,"line":167},[71,783,784],{},"    logger.LogError(ex, \"Unhandled exception\");\n",[71,786,787],{"class":73,"line":173},[71,788,789],{},"    return false; \u002F\u002F exception continues to propagate — stack trace preserved!\n",[71,791,792],{"class":73,"line":283},[71,793,137],{},[15,795,796,797,799,800,803],{},"The ",[19,798,695],{}," filter runs ",[44,801,802],{},"before the stack unwinds"," — this is why it's preferred for\nlogging: you see the full stack in the log, not just up to the catch block.",[10,805,807],{"id":806},"exceptiondispatchinfo-capturing-exceptions-across-threads","ExceptionDispatchInfo — capturing exceptions across threads",[32,809,811],{"className":65,"code":810,"language":67,"meta":40,"style":40},"\u002F\u002F Capture an exception with its full stack trace:\nExceptionDispatchInfo? captured = null;\n\nvar thread = new Thread(() =>\n{\n    try { RiskyWork(); }\n    catch (Exception ex)\n    {\n        captured = ExceptionDispatchInfo.Capture(ex);\n    }\n});\nthread.Start();\nthread.Join();\n\n\u002F\u002F Re-throw on the calling thread — original stack trace preserved:\ncaptured?.Throw();\n",[19,812,813,818,823,827,832,836,841,846,850,855,859,864,869,874,878,883],{"__ignoreMap":40},[71,814,815],{"class":73,"line":74},[71,816,817],{},"\u002F\u002F Capture an exception with its full stack trace:\n",[71,819,820],{"class":73,"line":80},[71,821,822],{},"ExceptionDispatchInfo? captured = null;\n",[71,824,825],{"class":73,"line":86},[71,826,96],{"emptyLinePlaceholder":95},[71,828,829],{"class":73,"line":92},[71,830,831],{},"var thread = new Thread(() =>\n",[71,833,834],{"class":73,"line":99},[71,835,119],{},[71,837,838],{"class":73,"line":105},[71,839,840],{},"    try { RiskyWork(); }\n",[71,842,843],{"class":73,"line":110},[71,844,845],{},"    catch (Exception ex)\n",[71,847,848],{"class":73,"line":116},[71,849,388],{},[71,851,852],{"class":73,"line":122},[71,853,854],{},"        captured = ExceptionDispatchInfo.Capture(ex);\n",[71,856,857],{"class":73,"line":128},[71,858,403],{},[71,860,861],{"class":73,"line":134},[71,862,863],{},"});\n",[71,865,866],{"class":73,"line":140},[71,867,868],{},"thread.Start();\n",[71,870,871],{"class":73,"line":145},[71,872,873],{},"thread.Join();\n",[71,875,876],{"class":73,"line":151},[71,877,96],{"emptyLinePlaceholder":95},[71,879,880],{"class":73,"line":156},[71,881,882],{},"\u002F\u002F Re-throw on the calling thread — original stack trace preserved:\n",[71,884,885],{"class":73,"line":162},[71,886,887],{},"captured?.Throw();\n",[15,889,890,891,894,895,898,899,901],{},"This is what ",[19,892,893],{},"await"," uses internally: when a Task faults, the exception is captured as\n",[19,896,897],{},"ExceptionDispatchInfo"," and re-thrown with the original stack trace when you ",[19,900,893],{}," the\nTask. You rarely need this directly.",[10,903,905],{"id":904},"global-exception-handling-in-aspnet-core","Global exception handling in ASP.NET Core",[907,908,910],"h3",{"id":909},"net-8-iexceptionhandler-preferred",".NET 8+ — IExceptionHandler (preferred)",[32,912,914],{"className":65,"code":913,"language":67,"meta":40,"style":40},"public class GlobalExceptionHandler : IExceptionHandler\n{\n    private readonly ILogger\u003CGlobalExceptionHandler> _logger;\n    public GlobalExceptionHandler(ILogger\u003CGlobalExceptionHandler> logger)\n        => _logger = logger;\n\n    public async ValueTask\u003Cbool> TryHandleAsync(\n        HttpContext context, Exception exception, CancellationToken ct)\n    {\n        _logger.LogError(exception, \"Unhandled exception for {Path}\", context.Request.Path);\n\n        (int status, string title) = exception switch\n        {\n            NotFoundException        => (404, \"Not Found\"),\n            ValidationException      => (422, \"Validation Error\"),\n            UnauthorizedException    => (401, \"Unauthorized\"),\n            _                        => (500, \"Internal Server Error\")\n        };\n\n        context.Response.StatusCode = status;\n        await context.Response.WriteAsJsonAsync(new ProblemDetails\n        {\n            Status = status,\n            Title  = title,\n            Detail = exception.Message\n        }, ct);\n\n        return true; \u002F\u002F exception handled\n    }\n}\n\n\u002F\u002F Register in Program.cs:\nbuilder.Services.AddExceptionHandler\u003CGlobalExceptionHandler>();\nbuilder.Services.AddProblemDetails();\napp.UseExceptionHandler();\n",[19,915,916,921,925,930,935,940,944,949,954,958,963,967,972,976,981,986,991,996,1001,1005,1010,1015,1019,1024,1029,1034,1039,1043,1048,1052,1056,1060,1065,1070,1075],{"__ignoreMap":40},[71,917,918],{"class":73,"line":74},[71,919,920],{},"public class GlobalExceptionHandler : IExceptionHandler\n",[71,922,923],{"class":73,"line":80},[71,924,119],{},[71,926,927],{"class":73,"line":86},[71,928,929],{},"    private readonly ILogger\u003CGlobalExceptionHandler> _logger;\n",[71,931,932],{"class":73,"line":92},[71,933,934],{},"    public GlobalExceptionHandler(ILogger\u003CGlobalExceptionHandler> logger)\n",[71,936,937],{"class":73,"line":99},[71,938,939],{},"        => _logger = logger;\n",[71,941,942],{"class":73,"line":105},[71,943,96],{"emptyLinePlaceholder":95},[71,945,946],{"class":73,"line":110},[71,947,948],{},"    public async ValueTask\u003Cbool> TryHandleAsync(\n",[71,950,951],{"class":73,"line":116},[71,952,953],{},"        HttpContext context, Exception exception, CancellationToken ct)\n",[71,955,956],{"class":73,"line":122},[71,957,388],{},[71,959,960],{"class":73,"line":128},[71,961,962],{},"        _logger.LogError(exception, \"Unhandled exception for {Path}\", context.Request.Path);\n",[71,964,965],{"class":73,"line":134},[71,966,96],{"emptyLinePlaceholder":95},[71,968,969],{"class":73,"line":140},[71,970,971],{},"        (int status, string title) = exception switch\n",[71,973,974],{"class":73,"line":145},[71,975,650],{},[71,977,978],{"class":73,"line":151},[71,979,980],{},"            NotFoundException        => (404, \"Not Found\"),\n",[71,982,983],{"class":73,"line":156},[71,984,985],{},"            ValidationException      => (422, \"Validation Error\"),\n",[71,987,988],{"class":73,"line":162},[71,989,990],{},"            UnauthorizedException    => (401, \"Unauthorized\"),\n",[71,992,993],{"class":73,"line":167},[71,994,995],{},"            _                        => (500, \"Internal Server Error\")\n",[71,997,998],{"class":73,"line":173},[71,999,1000],{},"        };\n",[71,1002,1003],{"class":73,"line":283},[71,1004,96],{"emptyLinePlaceholder":95},[71,1006,1007],{"class":73,"line":406},[71,1008,1009],{},"        context.Response.StatusCode = status;\n",[71,1011,1012],{"class":73,"line":411},[71,1013,1014],{},"        await context.Response.WriteAsJsonAsync(new ProblemDetails\n",[71,1016,1017],{"class":73,"line":416},[71,1018,650],{},[71,1020,1021],{"class":73,"line":422},[71,1022,1023],{},"            Status = status,\n",[71,1025,1026],{"class":73,"line":428},[71,1027,1028],{},"            Title  = title,\n",[71,1030,1031],{"class":73,"line":434},[71,1032,1033],{},"            Detail = exception.Message\n",[71,1035,1036],{"class":73,"line":439},[71,1037,1038],{},"        }, ct);\n",[71,1040,1041],{"class":73,"line":445},[71,1042,96],{"emptyLinePlaceholder":95},[71,1044,1045],{"class":73,"line":451},[71,1046,1047],{},"        return true; \u002F\u002F exception handled\n",[71,1049,1050],{"class":73,"line":641},[71,1051,403],{},[71,1053,1054],{"class":73,"line":647},[71,1055,137],{},[71,1057,1058],{"class":73,"line":653},[71,1059,96],{"emptyLinePlaceholder":95},[71,1061,1062],{"class":73,"line":659},[71,1063,1064],{},"\u002F\u002F Register in Program.cs:\n",[71,1066,1067],{"class":73,"line":665},[71,1068,1069],{},"builder.Services.AddExceptionHandler\u003CGlobalExceptionHandler>();\n",[71,1071,1072],{"class":73,"line":671},[71,1073,1074],{},"builder.Services.AddProblemDetails();\n",[71,1076,1077],{"class":73,"line":677},[71,1078,1079],{},"app.UseExceptionHandler();\n",[907,1081,1083],{"id":1082},"pre-net-8-useexceptionhandler-middleware","Pre-.NET 8 — UseExceptionHandler middleware",[32,1085,1087],{"className":65,"code":1086,"language":67,"meta":40,"style":40},"app.UseExceptionHandler(errorApp =>\n{\n    errorApp.Run(async context =>\n    {\n        var feature = context.Features.Get\u003CIExceptionHandlerFeature>();\n        var ex = feature?.Error;\n        logger.LogError(ex, \"Unhandled exception\");\n        context.Response.StatusCode = 500;\n        await context.Response.WriteAsJsonAsync(new { error = \"Internal server error\" });\n    });\n});\n",[19,1088,1089,1094,1098,1103,1107,1112,1117,1122,1127,1132,1136],{"__ignoreMap":40},[71,1090,1091],{"class":73,"line":74},[71,1092,1093],{},"app.UseExceptionHandler(errorApp =>\n",[71,1095,1096],{"class":73,"line":80},[71,1097,119],{},[71,1099,1100],{"class":73,"line":86},[71,1101,1102],{},"    errorApp.Run(async context =>\n",[71,1104,1105],{"class":73,"line":92},[71,1106,388],{},[71,1108,1109],{"class":73,"line":99},[71,1110,1111],{},"        var feature = context.Features.Get\u003CIExceptionHandlerFeature>();\n",[71,1113,1114],{"class":73,"line":105},[71,1115,1116],{},"        var ex = feature?.Error;\n",[71,1118,1119],{"class":73,"line":110},[71,1120,1121],{},"        logger.LogError(ex, \"Unhandled exception\");\n",[71,1123,1124],{"class":73,"line":116},[71,1125,1126],{},"        context.Response.StatusCode = 500;\n",[71,1128,1129],{"class":73,"line":122},[71,1130,1131],{},"        await context.Response.WriteAsJsonAsync(new { error = \"Internal server error\" });\n",[71,1133,1134],{"class":73,"line":128},[71,1135,680],{},[71,1137,1138],{"class":73,"line":134},[71,1139,863],{},[10,1141,1143],{"id":1142},"throw-expressions-c-7","throw expressions (C# 7)",[32,1145,1147],{"className":65,"code":1146,"language":67,"meta":40,"style":40},"\u002F\u002F Null-coalescing throw:\n_repo = repo ?? throw new ArgumentNullException(nameof(repo));\n\n\u002F\u002F In ternary:\nstring GetName(User? u) => u != null ? u.Name : throw new ArgumentNullException(nameof(u));\n\n\u002F\u002F In expression-bodied member:\npublic string Name\n{\n    set => _name = value ?? throw new ArgumentNullException(nameof(value));\n}\n\n\u002F\u002F .NET 6+ throw helpers — even cleaner:\nArgumentNullException.ThrowIfNull(repo);\nArgumentException.ThrowIfNullOrEmpty(name);\nArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);\n",[19,1148,1149,1154,1159,1163,1168,1173,1177,1182,1187,1191,1196,1200,1204,1209,1214,1219],{"__ignoreMap":40},[71,1150,1151],{"class":73,"line":74},[71,1152,1153],{},"\u002F\u002F Null-coalescing throw:\n",[71,1155,1156],{"class":73,"line":80},[71,1157,1158],{},"_repo = repo ?? throw new ArgumentNullException(nameof(repo));\n",[71,1160,1161],{"class":73,"line":86},[71,1162,96],{"emptyLinePlaceholder":95},[71,1164,1165],{"class":73,"line":92},[71,1166,1167],{},"\u002F\u002F In ternary:\n",[71,1169,1170],{"class":73,"line":99},[71,1171,1172],{},"string GetName(User? u) => u != null ? u.Name : throw new ArgumentNullException(nameof(u));\n",[71,1174,1175],{"class":73,"line":105},[71,1176,96],{"emptyLinePlaceholder":95},[71,1178,1179],{"class":73,"line":110},[71,1180,1181],{},"\u002F\u002F In expression-bodied member:\n",[71,1183,1184],{"class":73,"line":116},[71,1185,1186],{},"public string Name\n",[71,1188,1189],{"class":73,"line":122},[71,1190,119],{},[71,1192,1193],{"class":73,"line":128},[71,1194,1195],{},"    set => _name = value ?? throw new ArgumentNullException(nameof(value));\n",[71,1197,1198],{"class":73,"line":134},[71,1199,137],{},[71,1201,1202],{"class":73,"line":140},[71,1203,96],{"emptyLinePlaceholder":95},[71,1205,1206],{"class":73,"line":145},[71,1207,1208],{},"\u002F\u002F .NET 6+ throw helpers — even cleaner:\n",[71,1210,1211],{"class":73,"line":151},[71,1212,1213],{},"ArgumentNullException.ThrowIfNull(repo);\n",[71,1215,1216],{"class":73,"line":156},[71,1217,1218],{},"ArgumentException.ThrowIfNullOrEmpty(name);\n",[71,1220,1221],{"class":73,"line":162},[71,1222,1223],{},"ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);\n",[10,1225,1227],{"id":1226},"checked-vs-unchecked-arithmetic","checked vs unchecked arithmetic",[32,1229,1231],{"className":65,"code":1230,"language":67,"meta":40,"style":40},"int max = int.MaxValue;\n\n\u002F\u002F Unchecked (default): wraps silently\nint overflow = max + 1; \u002F\u002F -2,147,483,648 — no exception!\n\n\u002F\u002F Checked: throws OverflowException\nchecked\n{\n    int safe = max + 1; \u002F\u002F OverflowException\n}\n\n\u002F\u002F Hash code computation intentionally overflows — use unchecked:\nunchecked\n{\n    int hash = 17;\n    hash = hash * 31 + value.GetHashCode();\n    return hash;\n}\n",[19,1232,1233,1238,1242,1247,1252,1256,1261,1266,1270,1275,1279,1283,1288,1293,1297,1302,1307,1312],{"__ignoreMap":40},[71,1234,1235],{"class":73,"line":74},[71,1236,1237],{},"int max = int.MaxValue;\n",[71,1239,1240],{"class":73,"line":80},[71,1241,96],{"emptyLinePlaceholder":95},[71,1243,1244],{"class":73,"line":86},[71,1245,1246],{},"\u002F\u002F Unchecked (default): wraps silently\n",[71,1248,1249],{"class":73,"line":92},[71,1250,1251],{},"int overflow = max + 1; \u002F\u002F -2,147,483,648 — no exception!\n",[71,1253,1254],{"class":73,"line":99},[71,1255,96],{"emptyLinePlaceholder":95},[71,1257,1258],{"class":73,"line":105},[71,1259,1260],{},"\u002F\u002F Checked: throws OverflowException\n",[71,1262,1263],{"class":73,"line":110},[71,1264,1265],{},"checked\n",[71,1267,1268],{"class":73,"line":116},[71,1269,119],{},[71,1271,1272],{"class":73,"line":122},[71,1273,1274],{},"    int safe = max + 1; \u002F\u002F OverflowException\n",[71,1276,1277],{"class":73,"line":128},[71,1278,137],{},[71,1280,1281],{"class":73,"line":134},[71,1282,96],{"emptyLinePlaceholder":95},[71,1284,1285],{"class":73,"line":140},[71,1286,1287],{},"\u002F\u002F Hash code computation intentionally overflows — use unchecked:\n",[71,1289,1290],{"class":73,"line":145},[71,1291,1292],{},"unchecked\n",[71,1294,1295],{"class":73,"line":151},[71,1296,119],{},[71,1298,1299],{"class":73,"line":156},[71,1300,1301],{},"    int hash = 17;\n",[71,1303,1304],{"class":73,"line":162},[71,1305,1306],{},"    hash = hash * 31 + value.GetHashCode();\n",[71,1308,1309],{"class":73,"line":167},[71,1310,1311],{},"    return hash;\n",[71,1313,1314],{"class":73,"line":173},[71,1315,137],{},[10,1317,1319],{"id":1318},"when-to-throw-vs-return-an-error","When to throw vs return an error",[1321,1322,1323,1336],"table",{},[1324,1325,1326],"thead",{},[1327,1328,1329,1333],"tr",{},[1330,1331,1332],"th",{},"Situation",[1330,1334,1335],{},"Prefer",[1337,1338,1339,1349,1367,1375,1388],"tbody",{},[1327,1340,1341,1345],{},[1342,1343,1344],"td",{},"Programming error (bug), invalid state",[1342,1346,1347],{},[19,1348,187],{},[1327,1350,1351,1354],{},[1342,1352,1353],{},"Expected failure in a control-flow",[1342,1355,1356,1357,1360,1361,1360,1364],{},"return ",[19,1358,1359],{},"bool"," \u002F ",[19,1362,1363],{},"Result\u003CT>",[19,1365,1366],{},"null",[1327,1368,1369,1372],{},[1342,1370,1371],{},"Async — always",[1342,1373,1374],{},"Store in Task, re-throw on await",[1327,1376,1377,1380],{},[1342,1378,1379],{},"Validation at API boundary",[1342,1381,1382,295,1385],{},[19,1383,1384],{},"ValidationException",[19,1386,1387],{},"ProblemDetails",[1327,1389,1390,1393],{},[1342,1391,1392],{},"Recoverable business failure",[1342,1394,1395],{},"Return a Result\u002Fdiscriminated union",[15,1397,1398,1399,499,1402,1405],{},"Libraries like ",[19,1400,1401],{},"FluentValidation",[19,1403,1404],{},"ErrorOr"," provide Result types for richer\nerror handling without exception overhead in hot paths.",[10,1407,1409],{"id":1408},"recap","Recap",[15,1411,1412,1413,1415,1416,1418,1419,295,1421,1424,1425,1427,1428,1430,1431,1434,1435,1438],{},"Use bare ",[19,1414,187],{}," to re-throw — never ",[19,1417,21],{},". The stack trace is your debugging\nlifeline: preserve it. Use ",[19,1420,291],{},[19,1422,1423],{},"using"," for cleanup. Design custom exceptions\nonly when callers need programmatic distinction; include domain context as typed properties.\n",[19,1426,506],{}," wraps parallel task failures — inspect individual Task exceptions for\nfull coverage. ",[19,1429,695],{}," filters add conditions without unwinding the stack, making them ideal\nfor selective catching and \"log and rethrow\" patterns. In ASP.NET Core, centralise HTTP\nerror mapping in a global ",[19,1432,1433],{},"IExceptionHandler"," rather than scattering status-code decisions\nacross controllers. Use ",[19,1436,1437],{},"ArgumentNullException.ThrowIfNull"," and sibling helpers (.NET 6+)\nfor clean input validation at method boundaries.",[1440,1441,1442],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":40,"searchDepth":80,"depth":80,"links":1444},[1445,1446,1447,1448,1449,1450,1451,1452,1453,1457,1458,1459,1460],{"id":12,"depth":80,"text":13},{"id":29,"depth":80,"text":30},{"id":61,"depth":80,"text":62},{"id":191,"depth":80,"text":192},{"id":302,"depth":80,"text":303},{"id":492,"depth":80,"text":493},{"id":688,"depth":80,"text":689},{"id":806,"depth":80,"text":807},{"id":904,"depth":80,"text":905,"children":1454},[1455,1456],{"id":909,"depth":86,"text":910},{"id":1082,"depth":86,"text":1083},{"id":1142,"depth":80,"text":1143},{"id":1226,"depth":80,"text":1227},{"id":1318,"depth":80,"text":1319},{"id":1408,"depth":80,"text":1409},"Exception handling in C# — why throw ex destroys the stack trace, when to write custom exceptions, how AggregateException works in async code, and setting up a global handler in ASP.NET Core.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-exception-handling-best-practices","\u002Fdotnet\u002Fcsharp-core\u002Fexceptions",{"title":5,"description":1461},"blog\u002Fdotnet-exception-handling-best-practices","Exception Handling","C# Core","csharp-core","2026-06-23","sE2RwY_1eqhEWvuWExAEUi9MbWat1U_Wb7vtwx_lktY",1782244087153]