> This means that exceptions will be effectively used for flow control as I will have to use try/catch to branch.

I want to disambiguate an important point here, are you're likely going to get (technically correct) advice that might answer a different question than what you're asking.

Exceptions are _intended_ to be used for flow control (i.e. throwing and catching). There's no other way to correctly use exceptions to do what they were designed to do.

The bad practice would using exceptions to control a non-exceptional flow of your application, i.e. misusing the flow control to direct your application for anything other than a situation that cannot be resolved.

>  You can't just kill the client app if one request fails (or maybe you can?). I can think of some workarounds here like a message bus that handles the exception and displays the message in an universal way but what about unlocking the form and what about these 409 responses [..]?

You're getting confused between exceptions and bad results. When an exception is caught and you then return a bad result in your request (i.e. a non-200 status code), it ceases to be an exception, and instead is now functionally speaking a message to the consumer (i.e. the client app).

What you do with that result is up to your client application. Maybe you show it to the user. Maybe you silently log it. Maybe you swallow it intentionally (if you have a good reason to). Maybe you terminate your client application.

What your client app should do isn't answerable in this question, since we don't know the specs or user expectations.

> 409 Conflict when the error is one that should be displayed to the user

That is not how a [409 http status](https://httpstatuses.com/409) is defined. Are you maybe using 409s in a homebrew approach?

All responses from the server  (status code and response object), *no matter the status code*, are intended to be viewed by the consumer, since the consumer is the client app (not the person using the app!). 

The entire purpose of the returned information (status code and response object) is so the server can relay information to its consumer. If the consumer wouldn't be allowed to see it, that'd defeat the purpose entirely.

Whether your client app then relays that information to _its_ consumer (i.e. the end user), is contextual, as I already mentioned in the previous section. Whether your client app reuses the message it receives from the server, or it instead created its own message, is completely up to the client app to decide.

It seems like you are trying to use the 409 status to let the server decide whether the client app should or should not expose a certain server message to the user. That's not how you should be approaching this. 

The server returns a result to a consumer (whether that's a client app, Postman, or an actual person firing off web requests), and it shouldn't care about what the consumer then does with the data it receives.

> My final idea is to create Result types designed to be used with pattern matching. For example like this

This is one of those "just because you _can_ do it doesn't mean you _should_ do it" situations.

* Empty base classes are a misuse of OOP principles. There is nothing to gain by having this base type since the base type does not define any reusable implementation (nor contract).

* The functional difference between `Error` and `ExpectedError` can and should be expressed using a boolean property, not a new derived type.

* `userResult is Success<User> success` is polymorphism abuse. This too can be implemented using a simple scalar property, i.e. `userResult.IsSuccess`

The result object can be condensed into a single type and used accordingly. There is no reason to separate these types. However, I do understand where your intention comes from. This can be achieved using a different approach, e.g. static methods that define success/error states:

    public class Result<T>
    {
        public bool IsSuccess { get; private set; }
        public T Value { get; private set }
        public string Message { get; private set; }
        public bool IsExpectedError { get; private set; }
    

        private Result() { }

        public static Result<T> Success(T value)
        {
            return new Result<T>()
            {
                IsSuccess = true,
                Value = value
            };
        }

        public static Result<T> Error(string message, bool isExpectedError = false)
        {
            return new Result<T>()
            {
                IsSuccess = false,
                Message = message,
                IsExpectedError = isExpectedError 
            };
        }
    }

The private constructor ensures that you can only create `Result<>` objects using the defined static methods, and the static methods themselves specifically force the developer to fill in particular values for a particular result state (success/error). Usage examples:

    var goodResult = Result<User>.Success(myUser);
    var errorResult = Result<User>.Error("this is a message");

_As a sidenote, I don't quite agree with the name/usage of an "expected" error, but I already addressed this in and earlier section. I retained its usage here to show how your code can be changed to avoid OOP abuse._

> * Craft response objects.  
> * My final idea is to create Result types designed to be used with pattern matching

The response object (considering the improvements I just suggested) are really good result objects for the web response, i.e. the communication between backend and frontend.

However, these are not appropriate to replace exceptions in the internal logic of the backend itself. The .NET ecosystem has been built on exceptions, and you'll always have to deal with the possibility of exceptions being thrown. 

Maybe you don't throw exceptions yourself, but the libraries and framework tools you rely on will be throwing exceptions and you should be catching them (at the very least on the top level just before it would otherwise be returned to the consumer of your backend).

If you're already going to be stuck handling exceptions anyway, there's no point to then also rolling your own result object for a subset of cases. If the error states of your result object are functionally equivalent to what otherwise would've been a thrown exception, you can just throw an exception.