Skip to content

Add mechanism for error recovery with a different contract #54

@neefrehman

Description

@neefrehman

Opening this issue as a follow up to the proposal from #12 (which was partially implemented in #47 but only for recovery with the same contract) to ensure the error rerouting use case is still tracked.

Use case

Being able to change the signature of a Result after a recovery attempt would be really useful for our usage of better-result. Our backend returns Results directly to various clients, and the instrumentation and logic in some of those clients means that some errors are actually unexpected and so we don't want to handle them explicitly. Right now we deal with this in a slightly awkward way:

const mutation = useMutation({
  mutationFn: async () => {
    const result: Result<string, InvalidInputError | UnauthorizedError | NameAlreadyTakenError> = await api.closeIssue(issueId);

    if (result.isErr()) {
      matchErrorPartial(
        result.error,
        {
          // `NameAlreadyTakenError` is expected, we want to add some custom UI in this case and convert it to a success to be able to do that using `mutation.data`
          NameAlreadyTakenError: (e) => Result.ok(e), // Could also return anything that lets us identify the error, and we could trigger related side-effects in this callback
        },
        (e) => {
          // `UnauthorizedError` is unexpected for this client only, unauthz-ed users should not get this far
          // `InvalidInput` is unexpected for this client only, the frontend should have prevented this mutation from happening via form validation
          // In both cases we want to throw this error so it gets picked up by our app's error instrumentation and logged
          throw e;
        },
      );
      return
    }

    return result.value;
  },
});

This example is overly simplified. We often have >10 known error cases for some API calls, many of which are unexpected for some clients. It would be much more ergonomic if we could deal with these scenarios with better support from the library. An API like this comes to mind:

const mutation = useMutation({
  mutationFn: async () => {
    const result: Result<string, InvalidInputError | UnauthorizedError | NameAlreadyTakenError> = await api.closeIssue(issueId);

    return result
      .reroute(
        matchErrorPartial(
          {
            NameAlreadyTakenError: (e) => Result.ok(e),
          },
          (e) => e, // as a side-note, it would be great if this was the default second argument to `matchErrorPartial`
        ),
      )
      .unwrap();
  },
});

I appreciate it's a little odd to convert errors to successes like this, but it's the most immediately solution that comes to mind. I did consider a separate approach that allows us to "pick" specific Err cases out of the Result and then throw them explicitly, but this approach feels simpler and more idiomatic to the library.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions