On this pageOverview
Commands
You're probably wondering how to handle side effects like HTTP requests, timers, or interacting with the browser API. In Foldkit, side effects are managed through commands returned by the update function. This keeps your update logic pure and testable.
Let's start simple. Say we want to wait one second before resetting the count if the user clicks reset. This is how we might implement that:
import { Effect, Match as M } from 'effect'
import { Task } from 'foldkit'
import { Command } from 'foldkit/command'
const ClickedResetAfterDelay = m('ClickedResetAfterDelay')
const ElapsedResetDelay = m('ElapsedResetDelay')
const update = (
model: Model,
message: Message,
): [Model, ReadonlyArray<Command<Message>>] =>
M.value(message).pipe(
M.withReturnType<[Model, ReadonlyArray<Command<Message>>]>(),
M.tagsExhaustive({
ClickedResetAfterDelay: () => [
model,
[Task.delay('1 second').pipe(Effect.as(ElapsedResetDelay()))],
],
ElapsedResetDelay: () => [0, []],
}),
)Now, what if we want to get the next count from an API instead of incrementing locally? We can create a Command that performs the HTTP request and returns a Message when it completes:
import { Effect, Match as M, Schema } from 'effect'
import { Command } from 'foldkit/command'
import { m } from 'foldkit/message'
const ClickedFetchCount = m('ClickedFetchCount')
const SucceededCountFetch = m('SucceededCountFetch', {
count: Schema.Number,
})
const FailedCountFetch = m('FailedCountFetch', {
error: Schema.String,
})
const update = (
model: Model,
message: Message,
): [Model, ReadonlyArray<Command<Message>>] =>
M.value(message).pipe(
M.withReturnType<[Model, ReadonlyArray<Command<Message>>]>(),
M.tagsExhaustive({
// Tell Foldkit to fetch the count from the API
ClickedFetchCount: () => [model, [fetchCount]],
// Update the count on successful API response
SucceededCountFetch: ({ count }) => [count, []],
// Keep the current count on failure
FailedCountFetch: ({ error }) => {
// We could also update our model to include the error message
// and display it in the view.
return [model, []]
},
}),
)
// Command that fetches the count from an API
const fetchCount: Command<
typeof SucceededCountFetch | typeof FailedCountFetch
> = Effect.gen(function* () {
const result = yield* Effect.tryPromise(() =>
fetch('/api/count').then(res => {
if (!res.ok) throw new Error('API request failed')
return res.json() as unknown as { count: number }
}),
)
return SucceededCountFetch({ count: result.count })
}).pipe(
Effect.catchAll(error =>
Effect.succeed(FailedCountFetch({ error: error.message })),
),
)Let's zoom in on fetchCount to understand what's happening here:
import { Effect } from 'effect'
import { Command } from 'foldkit/command'
const fetchCount: Command<
typeof SucceededCountFetch | typeof FailedCountFetch
> = Effect.gen(function* () {
// tryPromise creates an Effect that represents an asynchronous computation
// that might fail. If the Promise rejects, it is propagated to the error channel
// in the Effect as UnknownException.
// https://effect.website/docs/getting-started/creating-effects/#trypromise
const result = yield* Effect.tryPromise(() =>
fetch('/api/count').then(res => {
if (!res.ok) throw new Error('API request failed')
// NOTE: We would not cast in a real application. Instead, we would
// decode the JSON using Effect Schema. For simplicity, we skip that here.
return res.json() as unknown as { count: number }
}),
)
// If we reach this, the Effect above that uses tryPromise succeeded,
// and we can return the SucceededCountFetch message
return SucceededCountFetch({ count: result.count })
}).pipe(
// We are forced by the type system to handle the error case because
// Command's may not fail. They must always return a Message. Here, we recover
// from failure by returning a FailedCountFetch Message with the error message.
// In a real application, we might log the error to an external service,
// retry the request, etc.
Effect.catchAll(error =>
Effect.succeed(FailedCountFetch({ error: error.message })),
),
)Errors are tracked, not hidden
Commands use Effect’s typed error channel — if a command can fail, the type signature tells you. Effect.catchAll turns failures into messages like FailedWeatherFetch, and once all errors are handled, the type confirms it. The update function handles errors the same way it handles success: as facts about what happened.