Dotnet logo

.NET Tools

Essential productivity kit for .NET and game developers

.NET Tools

.NET Annotated Monthly | October 2023

The first program to replicate itself was “The Creeper”, essentially being the first computer virus. The software would seat itself in a machine and print “I’m the creeper, catch me if you can!”. But no virus is complete without its nemesis, and in this case it’s “The Reaper”. It was the first (antivirus) program built to eradicate The Creeper. From there, we all know how it plays out.

.NET Annoated Monthly | October 2023

.NET news

Featured content 

We’d like to thank Paul Blasucci for curating this month’s featured content! He is passionate – yet pragmatic – about a multi-paradigm and polyglot approach to writing software. He has spent the past 25 years blending a disparate array of languages, technologies, and methodologies to develop compelling solutions to a wide range of business problems. Paul especially enjoys solving challenges in heterogeneous enterprise systems. His weblog,, is irregularly updated with various musings on software development, and he explores the fediverse as When not at the keyboard, Paul may be found globetrotting with his wife and children (though his soul is still catchin’ waves at the Jersey Shore).

I’m thrilled to be given this opportunity to engage the community. Thanks JetBrains! As a long-time .NET developer, one topic about which I am passionate is Error Handling Failure Management. Thanks, readers, for indulging me…

Software will fail. It’s not a question of “if”, or even “when”. Rather, it’s about what developers can do to both minimize failure and recover gracefully (if at all possible). There are many approaches one might take. However, in the.NET ecosystem, it is not uncommon to find developers haven’t truly given the topic due consideration. In particular, there seems to either be a lot of focus on handling failures, or a lot of focus on communicating failures. However, a robust program really needs to address both.

Communicating Failure

When a piece of code wants to notify callers that something is wrong, there are three common approaches:

  • Panic 
  • Raise an exception
  • Use a purpose-build return type

ASIDE: Actors Fail Fast

Actor systems, like those found in Elixir, or Orleans, or Akka.NET, often take a strategy of “fail fast”, whereby panics are the main form of indicating something went wrong. However, such systems are beyond the scope of this article, as they redefine the notion of “executable process”. Still, they present a fascinating tool for building robust software, and are definitely worth studying.

It is vital to note that no one approach is exclusive of the other two. That is, a robust program will combine these mechanisms as needed. However, each of these modalities is best paired with a particular kind of failure. Panics are reserved for when something truly unrecoverable has gone seriously wrong. For example, a necessary piece of configuration is unavailable during program start-up.

let configureDatabase
  (configuration : IConfiguration)
  (services : IServiceCollection)
 : IServiceCollection
  match configuration.GetConnectionString("default") with
  | Empty -> ``panic!`` "No database connection details found!"
  | Trimmed connectionString ->
      let dataSource = NpgsqlDataSourceBuilder(connectionString)

Exceptions are for failures which are, well, exceptional. That is, they are not part of the ordinary program flow. Often they are very unexpected. However, a surprising number of exceptions may be anticipated as _potential_ failures. Usually, this is closely related to infrastructure concerns. For example, receiving a 503 Service Unavailable response to an HTTP request is not part of routine program flow. But code can predict it might occur, and plan some strategy for re-submitting the HTTP request.

let submitScores
  (client : HttpClient)
  (cancel : CancellationToken)
  (request : SubmitScoresRequest)
  task {
    let request' = buildHttpRequest request
      let! response = client.SendAsync(request', cancel)
      return unpackHttpResponse response
    with :? HttpRequestException as ex ->
      return! ex |> rescheduleHttpRequest client cancel request

Finally, failures which emanate from the problem domain of the software itself
are fantastic candidates for purpose-built return types. A common example would
be validation: checking that a piece of inventory is in stock before it can
be shipped.

let checkInventory
  (stock : IStockService)
  ({ Sku = itemCode; Quantity = count } as item)
  : Result<LineItem, InventoryProblem>
  match stock.ItemQuantity(itemCode) with
  | 0 -> Error(OutOfStock itemCode)
  | n when n < count ->
    let notEnough = InsufficientSupply(itemCode, count)
    Error notEnough
  | _ -> Ok item

Handling Failure

Responding to a particular failure is, by and large, determined by how said failure is communicated. If some code panics, the only possible response is to restart the process. Similarly, if one expects an exception, one traps it and then chooses the best course of action. Finally, a return type will need to have its value(s) inspected in order to select the appropriate program flow.

However, the important detail is – panics notwithstanding – the point-in-execution where an exception is trapped or a problem is revealed, is also the point-in-execution where the modality can be changed. That is, once an exception is trapped, it can be downgraded into a problem.

DbResult ArchiveReport(Report report)
   _db.Insert(report.ToArchivalFormat(_logger, _clock));
      return DbResult.Success(report.ReferenceCode);
  catch(SqlException x) when (x.Number is 2601)
      return DbResult.DuplicateArchivalAttempt(report.ReferenceCode);

Similarly, at some path in a program’s execution (usually near the top of a call chain), it maybe be reasonable to promote a problem into an exception.

webApp.MapGet("/report/{referenceCode}", (referenceCode, archive) =>
 archive.TryGetReport(referenceCode, out var report) switch
    true => report,
    false => throw new UnknownReportException(referenceCode)


Failure management in .NET is a nuanced and far-ranging topic. However, by keeping several key guidelines in mind, it is possible to build more robust software. To recap:

  • Failures may be communicated by: panicking, raising an exception, or returning data.
  • Panics can only be handled by relaunching the executing process.
  • Exceptions should not be used for regular control flow, but for signaling abnormal program state.
  • Exceptions, once trapped, may be inspected and various programmatic responses taken.
  • Problems are regular types, returned from methods or functions, which indicate something went wrong.
  • Problems should closely align to the business domain which the program serves.
  • Problems are not abnormal or unexpected, and are a core part of program design.
  • Programs may convert between exceptions and problems as the scope of failure shifts between infrastructure and domain.

Hopefully, this short article (which really could’ve been a whole book!) highlights a more robust approach to managing the failures which inevitably must occur when designing, coding, and executing software. At the very least, it perhaps gives “food for thought”.

Further Reading

If you are interested in more detailed, and better constructed, explorations of this topic, please see the following links:

Programing tutorials and tips 

.NET tutorials and tips

Related programming tutorials and tips:

Interesting and cool stuff

And finally, the latest from JetBrains

Here’s a chance to catch up on JetBrains news that you might have missed:

⚒️ Check out our .NET Guide! Videos, tips, and tricks on .NET related topics. ⚒️

More posts…

Don’t miss this fantastic offer! CODE Magazine is offering a free subscription to JetBrains customers. Get your copy today!

Sharing is caring! So share content that you find useful with other readers. Don’t keep it to yourself! Send us an email with your suggestions for publication in future newsletters!

Subscribe to .NET Annotated

image description