Design problem: ease of use vs. guarantee of success

Here’s the main question: would you prefer your Elm library to be rigorous and consistent or simple, but sometimes a bit uncertain?

Context

I have a Vault type that stores information. The Vault type includes two functions:

  • There’s a function getData :String -> Vault -> Maybe Json.Encode.Value to get data from a given key.
  • There’s a function findData : String -> Vault -> Task Error VaultUpdate to create a task to get that data elsewhere if our local Vault doesn’t have the information yet.

A recommended course of action for the user is to use getData to get the information, and otherwise use findData if it isn’t there but they want it badly. If the information doesn’t exist at all, we can then have the task raise an Error to make it clear that the data doesn’t exist at all.

Challenge

My main challenge here is that the Task Error VaultUpdate is secretly a lot more complex:

  1. It needs to establish a connection.
  2. It needs to re-authenticate if the access token has expired.
  3. It needs to refresh tokens that have expired.
  4. It needs to ask for other metadata to understand the context of the information.
  5. It needs to get the information that the user wants.

If any of the early steps fail, it is reasonable to return an Error and let the user know something went wrong. However, if we made a crucial change at step 3, we might really need that VaultUpdate if we want to be able to keep finding data in the future - even if step 5 raises an error.

There are ways to solve this; but it eventually comes down to the task succeeding with a VaultUpdate even though the step that the user expected, failed. This could lead to false expectations that the Vault may have the data after running findData successfully. As a benefit, however, we can erase the Error option in the task and just have the user insert a VaultUpdate -> msg to make a Cmd msg.

Question

To summarize, is it better to guarantee “it the task succeeds, we’ve got what you want” or is it better to design the library in a way of “okay we don’t have it now but we might get it next time you try it” with the benefit of simplicity?

When I’ve had to do something like this, I’ve also tended to get confused about complex event sequences with multiple error points like that.

I usually end up thinking about it like this:

type VaultResult
    = Loaded Json.Encode.Value
    | Loading
    | NotLoaded
        { status : NotLoadedStatus
        , load : Task Error VaultUpdate
        }

type NotLoadedStatus
    = NeverRequested
    | LoadingError Error

getData : String -> Vault -> VaultResult

Here the VaultResult always gives the clear and complete status of the item, and the caller can choose whether to initiate loading or re-loading of the entry or whether not to. The idea would also be that initiating the load task would also transition the entry to the Loading state, which would also then prevent future callers from accidentally re-initating the load task while the first load request is in progress.

Note you could possibly use different error types instead of the same Error type for LoadingError and NotLoaded.load if that makes sense in your case.

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.