Skip to content

Refactor error handling with UserCanceledError#16

Merged
jdubar merged 2 commits intomasterfrom
Fix/Result-pattern-improvements
Nov 10, 2025
Merged

Refactor error handling with UserCanceledError#16
jdubar merged 2 commits intomasterfrom
Fix/Result-pattern-improvements

Conversation

@bechtd
Copy link
Copy Markdown
Collaborator

@bechtd bechtd commented Nov 10, 2025

Refactor error handling with UserCanceledError

  • Replaced string-based "user canceled" errors with a strongly-typed UserCanceledError class for improved type safety and readability.
  • Updated SaveBytesToFileAsync and SelectImportFileAsync methods to use the new error type, changing the return type of SaveBytesToFileAsync to Task<Result>.
  • Modified FileServiceTests to validate UserCanceledError and ensure proper error handling.
  • Updated SleepTable.razor.cs to use HasError<UserCanceledError>() for type-safe error checks.
  • Introduced the UserCanceledError class in RoyAppMaui.Models and improved overall code maintainability, readability, and test coverage.

- Replaced string-based "user canceled" errors with a strongly-typed `UserCanceledError` class for improved type safety and readability.
- Updated `SaveBytesToFileAsync` and `SelectImportFileAsync` methods to use the new error type, changing the return type of `SaveBytesToFileAsync` to `Task<Result>`.
- Modified `FileServiceTests` to validate `UserCanceledError` and ensure proper error handling.
- Updated `SleepTable.razor.cs` to use `HasError<UserCanceledError>()` for type-safe error checks.
- Introduced the `UserCanceledError` class in `RoyAppMaui.Models` and improved overall code maintainability, readability, and test coverage.
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Nov 10, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
RoyAppMaui/Models/UserCanceledError.cs 100.00% <100.00%> (ø)
RoyAppMaui/Services/Impl/FileService.cs 95.23% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator Author

@bechtd bechtd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FluentResults is a beautiful library. Read its docs. They are good. I know there are features that I don't know about yet.

Comment on lines -169 to +171
A.CallTo(() => fileSaver.SaveAsync(A<string>._, A<Stream>._, A<CancellationToken>._)).Returns(new FileSaverResult(null, exception));

string? returnedFilePathAsNullSignalsUserCanceled = null;
A.CallTo(() => fileSaver.SaveAsync(A<string>._, A<Stream>._, A<CancellationToken>._))
.Returns(new FileSaverResult(returnedFilePathAsNullSignalsUserCanceled, exception));
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's important, in the test, to not "bury the lead".

  1. I like the wrap A.CallTo... .Returns for the "Cause -> Effect" layout of it
  2. even though it is overly verbose, this is a test, and I want it very clear that the null coming back from this SaveAsync call, for the path is what signals the User Canceled

Comment on lines -177 to +180
Assert.Equal("user canceled", actual.Errors[0].Message);
Assert.Single(actual.Errors);
Assert.IsType<UserCanceledError>(actual.Errors[0]);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a UserCanceledError class, derived off of FluentResults.Error.

This specific error stood out more than others because you check for it being the cause, at least twice, and that can be handled a lot cleaner during the error check, as you will see.

if (result.IsFailed)
{
if (!result.Errors[0].Message.Contains("user canceled"))
if (!result.HasError<UserCanceledError>())
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of FluentResults built-in helper functions. It makes it a lot clearer AND SAFER to check for a Type (the Error class is a type) rather than a string flag. Inside the UserCanceledError, the error message is still user canceled just in case you ever wanted to display that or log that, but now it's encapsulated. You don't have to rely on that string for your "signal". The class type is so much more powerful and exact.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this - I (now) remember you doing this in one of the other projects.


namespace RoyAppMaui.Models;

public class UserCanceledError() : Error("user canceled");
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the beauty of the newer primary constructor syntax, especially when extending another class.

You don't even need a class body.
You can still add one, if you felt like you wanted to add more functionality to this, but mostly, you don't.

This is the power of OOP ... leveraging types (classes are types).

{
Result<List<Sleep>> GetSleepDataFromCsv(string filePath);
Task<Result<bool>> SaveBytesToFileAsync(byte[] buffer, string filePath);
Task<Result> SaveBytesToFileAsync(byte[] buffer, string filePath);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a pretty common "overkill" for Result pattern.

You put bool as the return type.
But, that internal type IS OPTIONAL.

Sometimes, as is the case here (a Save op.) there's really only 2 possibilities:

  1. It saved successfully
  2. Some Error comes back (a failure)

Result already handles BOTH!

Result.Ok IS SUCCESS! Hover over it. It is literally in the C# comment.
Result.Fail OR returning an Error type you've made (like with UserCanceledError) is Failure.

Unless you need some other type of data coming back, you really don't need anything redundant for Success flag.

You'll notice at the top, in the test, I deleted the line asserting the bool was true.
We already asserted the Result was Success. That's all we needed. (I saw that earlier and it made my "Spidey-sense" go off, but I couldn't see it, just yet.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can almost think of this like how Unix/Linux works.

When you run a command, if you get back the prompt, you can just ASSUME SUCCESS.
ONLY An Error message coming back means failure, otherwise, assume success. It's a hard thing to get used to when you aren't use to it, but Result.Ok() is sort of like this.

Ok IS ENOUGH of a signal.
Anything else would be a message from some error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another example.

OK with no content is like a 204 response code in HTTP.
200 code are supposed to contain something along with the success signal (some object or value, like an Id, from an Insert).

In many ways, especially when doing Web work, Result pattern maps very nicely to HTTP Response codes.

And, to a certain extent, you can look at your other error messages this way too ... a bit.

FileNotFoundException are like a 404
You could make a NotFoundError wherein you take in an error message (which would include the fact that this was a file not found.
OR, you could be more specific and make it FileNotFoundError and only take in the name of the file, as the argument.
OR, you could do both!

class NotFoundError(string message) : Error(message);
build NotFoundError off of Results Error, and then:
class FileNotFoundError(string filePath) : NotFoundError($"The file at {filePath} was not found.");

It's up to you how sophisticated and specific and layered you want to get.
Feel it out.
I prefer to use named Errors as opposed to Result.Fail(errorMessage

Because then I can test for them by their type, instead of using the "weaker" and bulkier "string flag" (is Equal to ... Contains)


return result.IsSuccessful
? Result.Ok(true)
? Result.Ok()
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, Ok() is enough. It means Success.

And yes, this is a common thing to do, until you get more comfortable with the library.

if (result is null)
{
return Result.Fail("user canceled");
return new UserCanceledError();
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting past the "string flag" mentality is going to take time.

I struggled with it. I'll still do it, at first. It's normal. We think in words, not "objects", not "types".

Updated `SaveBytesToFileAsync_SaveFails_ReturnsFailure` to use `OperationCanceledException` instead of a generic `Exception`.

Added an assertion to `SaveBytesToFileAsync_SaveSucceeds_ReturnsSuccess` to verify that the `Errors` collection is empty, ensuring thorough validation of the success scenario.
Copy link
Copy Markdown
Owner

@jdubar jdubar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much appreciated, Dave!

@jdubar jdubar added refactor cleanup and improve code coverage added or modified coverage tests labels Nov 10, 2025
@jdubar jdubar merged commit b92b414 into master Nov 10, 2025
4 checks passed
@jdubar jdubar deleted the Fix/Result-pattern-improvements branch November 10, 2025 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coverage added or modified coverage tests refactor cleanup and improve code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants