Improving error handling

This is part 4 of a series. You should probably start at the first part.

Okey, so last time we added some validations, but we had two issues at least :

  • the weird maybeToString function
  • Maybes all over the place felt weird
  • The rule “expectancy should be bigger than current age” was not validated.

Not reinventing the wheel

Turns out maybeToString can at least be done shorter by composing Maybe.withDefault and Maybe.map

maybeToString : Maybe Int -> String
maybeToString x =
    x
        |> Maybe.map toString
        |> Maybe.withDefault ""

I’ll leave it as a separate function, because, well, it’s small enough. And it turns out I might actually not need it.

A ‘Period’ vs ‘two strings in fields’

My main problem is that I want to keep track of a period, but, let’s face it : most of the time, I don’t really have a period. So let’s make peace with that, and just write :

type alias Model =
    { age : String
    , expected : String
    }


type alias Period =
    { age : Int
    , expected : Int
    }

Impresive, isn’t it ?

And I will add a function that converts those two strings to a period, or to an error type that indicates what is the problem.

type PeriodError
    = InvalidValues
    | InvalidAge
    | InvalidExpected
    | InvalidRange

Yes, I guess that covers all possible cases.

toPeriod : Model -> Result PeriodError Period
toPeriod model =
    let
        age =
            model.age
                |> String.toInt

        expected =
            model.expected
                |> String.toInt
    in
        case ( age, expected ) of
            ( Err _, Err _ ) ->
                Err InvalidValues

            ( Err _, _ ) ->
                Err InvalidAge

            ( _, Err _ ) ->
                Err InvalidExpected

            ( Ok age, Ok expected ) ->
                if age <= expected then
                    Ok { age = age, expected = expected }
                else
                    Err InvalidRange

This boringly parses stuff, and returns an error. This is also a natural place to check the ages.

From then on, everything just rolls on.

-- Yes, the model is now only strings
initialModel : Model
initialModel =
    { age = "35"
    , expected = "90"
    }

-- Actions are still the same
type Msg
    = Start
    | ChangeAge String
    | ChangeExpected String


init : ( Model, Cmd Msg )
init =
    ( initialModel
    , Cmd.none
    )


-- Actions simply udate the models
update : Msg -> Model -> ( Model, Cmd Msg )
update action model =
    case action of
        Start ->
            ( model, Cmd.none )

        ChangeAge a ->
            ( { model | age = a }, Cmd.none )

        ChangeExpected e ->
            ( { model | expected = e }, Cmd.none )

The views are hardly changed, except that we don’t need to coerce back into a String, since we hold… a String.

view : Model -> Html Msg
view model =
    div [ class "page" ]
        [ inputs model
        , tailendView model
        ]


inputs : Model -> Html Msg
inputs model =
    div []
        [ input
            [ placeholder "Age"
            , value model.age
            , onInput ChangeAge
            ]
            []
        , input
            [ placeholder "Expected"
            , value model.expected
            , onInput ChangeExpected
            ]
            []
        ]

And displaying the tailend becomes trivial.

tailendView : Model -> Html Msg
tailendView model =
    let
        period =
            toPeriod model
    in
        case period of
            Err e ->
                periodError e

            Ok p ->
                periodView p

With either a nice view :

periodView : Period -> HtmlMsg
periodView p =
    let
        crossed =
            p.age

        uncrossed =
            p.expected - p.age
    in
        div [ class "tailend-view" ]
            ((List.repeat crossed crossedItem) ++ (List.repeat uncrossed uncrossedItem))

Or an error message :

periodError : PeriodError -> Html Msg
periodError error =
    let
        message =
            case error of
                InvalidValues ->
                    "Please type an age and expected age"

                InvalidAge ->
                    "Please type an age"

                InvalidExpected ->
                    "Please type an expected age"

                InvalidRange ->
                    "Your expected age should be bigger than your age"
    in
        div [] [ text message ]

So, roughly speaking, things work. But, there is a ‘but’ :

  • everything is in the same file. I’m not sure how to refactor this. For example, ideally I would like to move the ‘inputs’ function, but it needs to ‘Msg’ type.
  • Better yet, I would like to hide everything related to the period picker in a module. But it seems like creating a ‘Period Picker’ component, and evan tells me not to do that !

So that’s the next thing to look into, probably…

As usual, the code is on Github