Retrying failed requests in Elm... without the headache
Tags: elm, learning, discuss, showdev
Published: 2018-06-29 23:06:39

---
title: Retrying failed requests in Elm... without the headache
published: true
description: a proposal for retrying failed requests in an intuitive way
tags: elm, learning, discussion, showdev
---

I needed a generic way to retry requests that fail. I did not want to litter my model with extra state or clutter my `update` function by having to handle additional cases for each request's code paths.

I found [panosoft/elm-cmd-retry](https://github.com/panosoft/elm-cmd-retry) but it depends on native code and meets neither of the requirements set above.

## Wishful Thinking

It should be intuitive to use —

```elm
decoder
    |> Http.get url
    |> Retry.send DataReceived
```

It should allow us to retry with configuration —

```elm
decoder
    |> Http.get url
    |> Retry.sendWith { retries = 3, interval = 10 * Time.second } DataReceived
```

## Implementation

Here's my best attempt at the `Retry` module. I tweaked the API thanks to some comments I received on the [Elm discourse](https://discourse.elm-lang.org/t/retrying-failed-requests-without-the-headache/1445). The revised implementation also makes it possible to retry any Task using `retry` or `retryWith`. It compiles and works as intended in my program.

At a glance —

```elm
default : Config
retry : Task x a -> Task x a
retryWith : Config -> Task x a -> Task x a
send : (Result Http.Error a -> msg) -> Http.Request a -> Cmd msg
sendWith : Config -> (Result Http.Error a -> msg) -> Http.Request a -> Cmd msg
```

Complete implementation —

```elm
module Retry exposing (Config, default, retry, retryWith, send, sendWith)

import Http
import Process
import Task exposing (Task)
import Time exposing (Time)


type alias Config =
    { retries : Int
    , interval : Time
    }


default : Config
default =
    { retries = 5
    , interval = 1 * Time.second
    }


retry : Task x a -> Task x a
retry =
    retryWith default


retryWith : Config -> Task x a -> Task x a
retryWith config task =
    task |> Task.onError (onError config task)


send : (Result Http.Error a -> msg) -> Http.Request a -> Cmd msg
send =
    sendWith default


sendWith : Config -> (Result Http.Error a -> msg) -> Http.Request a -> Cmd msg
sendWith config resultToMessage req =
    let
        task =
            Http.toTask req
    in
        task
            |> Task.onError (onError config task)
            |> Task.attempt resultToMessage


onError : Config -> Task x a -> x -> Task x a
onError config task error =
    if config.retries == 0 then
        let
            _ =
                Debug.log "failed retrying" error
        in
            Task.fail error
    else
        let
            _ =
                Debug.log ("retrying " ++ (toString config.retries)) error

            next =
                task
                    |> Task.onError (onError { config | retries = config.retries - 1 } task)
        in
            Process.sleep config.interval
                |> Task.andThen (always next)

```

#### How to use it

As demonstrated in the example below we meet our goals. Our model is not required to keep track of extra state and it's easy to convert any single-attempt request into a request that retries itself.

```elm
module SomeModule exposing (..)

import Retry
import User exposing (User)
...


type alias Model =
    { users : List User }


type Msg
    = UsersReceived (Result Http.Error (List User))


init : ( Model, Cmd Msg )
init =
    ( { users = [] }, getUsers )


getUsers : Cmd Msg
getUsers =
    (Decode.list User.decode)
        |> Http.get "/users.json"
        |> Retry.send UsersReceived


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        UsersReceived (Ok users) ->
            { model | users = users }
                ! []

        UsersReceived (Err e) ->
            let
                _ =
                    Debug.log "request failed" e
            in
                ( model, Cmd.none )
```

---

## Feedback

I'm new to Elm and I'm asking the community for general feedback on this module.

* Can you see problems I may encounter with it?
* Can you see ways in which it can be improved?
* Am I breaking any Elm conventions?

## Original API

Originally I had written the `Retry` module with `retry` and `retryWith` functions that returned a `Task`. This required the programmer to use `Task.attempt` to create the necessary `Cmd`. If possible, it's best to keep this implementation detail out of the programmer's mind entirely. The `Retry.send` and `Retry.sendWith` API remedies this problem

```elm
-- original API leaks Task abstraction
decoder
    |> Http.get url
    |> Retry.retry
    |> Task.attempt DataReceived

-- proposal
decoder
    |> Http.get url
    |> Retry.retry DataReceived

-- proposal with configuration
decoder
    |> Http.get url
    |> Retry.retryWith { ... } DataReceived

-- new API
decoder
    |> Http.get url
    |> Retry.send DataReceived

-- new API with configuration
decoder
    |> Http.get url
    |> Retry.sendWith { retries = 3, interval = 10 * Time.second } DataReceived
```