Learn / Errors and failures

A first example: a read that fails

One read, drawn so that a failure shows, and a button that asks again:

ts
import { Match, Option, Schema } from 'effect'
import type { Html, HtmlBuilder } from 'foldkit/html'
import { defineMessageUnion } from 'foldkit/message'
import type * as Update from 'foldkit/update'
import { Entity } from 'foldkit-entity'
import { Remote, RemoteData, type RemoteClient } from 'foldkit-remote'
import { Surface } from 'foldkit-surface'

const Project = Entity.define('Project', Schema.Struct({ id: Schema.String, name: Schema.String }))
const ProjectName = Entity.select(Project, { id: true, name: true })

const Model = Schema.Struct({ projectId: Schema.String, remote: Remote.Model })
type Model = typeof Model.Type

const Message = defineMessageUnion({ ...Remote.messages, ClickedTryAgain: {} })
type Message = typeof Message.Type

const App = Surface.application({
  Model,
  Message,
  initial: { projectId: 'p1', remote: Remote.initial },
  update: (model, message) => update(model, message),
})
const Data = Remote.make({ model: App.model.remote, entities: [Project] })

// What the screen reads, as a function of the Model.
const project = (model: Model) => Data.get(ProjectName, model.projectId)
const actives = { project: Data.active('Project', model => Option.some(project(model))) }

function update(model: Model, message: Message): Update.Return<Model, Message, RemoteClient> {
  if (Remote.reduces(message)) return { model: Data.reduce(model, message) }
  // ClickedTryAgain, the one Message left: ask again for what failed, and only that.
  return { model: Data.refresh(model, actives, { failed: true }) }
}

const view = (model: Model, h: HtmlBuilder<Message>): Html =>
  RemoteData.render(project(model).read(model), {
    loading: () => h.p([], ['Loading…']),
    notFound: () => h.p([], ['No such project.']),
    failed: error =>
      h.div(
        [],
        [
          h.p([], [`Could not load the project: ${error.message}`]),
          h.button([h.OnClick(Message.ClickedTryAgain())], ['Try again']),
        ],
      ),
    data: (project, freshness) =>
      h.div(
        [],
        [
          h.h1([], [project.name]),
          Match.valueTags(freshness, {
            Fresh: () => h.empty,
            Refreshing: () => h.p([], ['Updating…']),
            Stale: ({ error }) => h.p([], [`Not updated: ${error.message}`]),
          }),
        ],
      ),
  })

Data.subscriptions(actives) (or Data.wiring(actives)) runs the read; the Remote README shows that step and the client.

What each piece does, and does not do:

  • RemoteData.render folds the six states of a read into the four things a view draws. failed is reached only by a failure with nothing to show. A refresh that fails after the project was shown reaches data, with a Stale freshness carrying the error, so the name stays on screen.
  • notFound is not a failure. The server answered and has no such row: asking again would answer the same.
  • Data.refresh(…, { failed: true }) marks only what failed as due, and the read entry asks again. It performs no I/O itself. A read with nothing to show reads Loading meanwhile, and Failed again if it fails again.
  • Nothing here retries on a timer. Without the button, the failure stays until something else writes the value: a live patch, a mutation's answer, a page of a list.