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.renderfolds the six states of a read into the four things a view draws.failedis reached only by a failure with nothing to show. A refresh that fails after the project was shown reachesdata, with aStalefreshness carrying the error, so the name stays on screen.notFoundis 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 readsLoadingmeanwhile, andFailedagain 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.