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 but it depends on native code and meets neither of the requirements set above.
Wishful Thinking
It should be intuitive to use —
decoder
|> Http.get url
|> Retry.send DataReceived
It should allow us to retry with configuration —
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. 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 —
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 —
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.
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
-- 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
Top comments (2)
Nice post.
I don't immediately see any technical problems. I think you will have to deal with Tasks to support the functionality, but you could provide your own function on Retry to turn it into a
Cmd Msg
.Based on my experience, automatic retries can by problematic for user experience. It can make sense in scenarios like trying to sync some data in the background. But in cases where the user is waiting on the response, I like to notify users of any problem and leave their page/data in a state where they can choose to retry themselves.
Just my 2 cents, FWIW.
Helpful feedback, thank you. Maybe something like "Automatically retrying in X sec ..." combined with a "Retry now" button. Now that we talk about it, I feel like I've seen this kind of "retry" interface many times. I didn't realize I already had a good example to guide me!