Skip to content

Effect in five minutes

Ramose is built on Effect, a TypeScript library for describing work as values. You can use Ramose without learning it. Here is the 5% you will see, for someone who has built the first app.

A Ramose call is a description, not a result

Section titled “A Ramose call is a description, not a result”

db.q(query) does not fetch rows. It returns an Effect value: a description of a read, with the result type and the possible errors in its type. Nothing happens until something runs it. The same is true of db.transact(…), db.pull(…) and db.install().

A write is one all-or-nothing group of changes; Ramose calls it a transaction (glossary). That is why a write can be built in one place and run in another, and why a failed step stops the whole thing before anything is sent.

useLive, useQuery and usePull run reads and re-run them; useTransact().run runs a write from an event handler. You never see Effect:

examples/todos/src/App.tsx:14-25
const TodoList = () => {
const { rows, error } = useLive(db, todoQuery);
if (error !== undefined) return <p>offline…</p>;
if (rows === undefined) return <p>loading…</p>;
return (
<ul>
{rows.map((row) => (
<TodoRowView key={row.id} row={row} />
))}
</ul>
);
};
examples/reef/src/app/screens/BoardScreen.tsx:243-245
const { run } = useTransact({
onError: (error) => toast("error", errorMessage(error)),
});

run(effect) returns a Promise; a policy denial arrives in onError as a readable message.

Anywhere else — a script, a Worker, a test — Effect.runPromise(effect) runs one Effect and gives you a Promise. Reef opens a workspace this way:

examples/reef/src/app/ramose.ts:43
if (provision) await Effect.runPromise(provisionWorkspace(db, user));

Every db method can be run this way directly; nothing else needs to be set up.

A write’s body is a generator. Read yield* as await: it runs the step and gives you its result. Reef’s drag-and-drop is two steps:

examples/reef/src/app/mutations.ts:113-122
export const moveIssue = (
db: ReefDb,
issueId: number,
status: Status,
rank: number,
) =>
db.transact(function* (tx) {
yield* tx.add(issueId, Issue.status, status);
yield* tx.add(issueId, Issue.rank, rank);
});

function* and yield* are plain JavaScript; Effect uses them so a write reads top to bottom. If any step fails, the write is not sent.

Every failure a db call can produce is a named error: TxRejected, Unauthorized, QueryBudgetExceeded, Unavailable, InvalidRequest, DatabaseNotFound, InternalError, NetworkError. Effect.catchTags handles the ones you care about by name:

examples/kv-style/app.ts:128-143
Effect.catchTags({
TxRejected: (e) =>
HttpServerResponse.json({ error: e.message, code: e.code }, { status: 409 }),
Unavailable: (e) =>
HttpServerResponse.json(
{ error: e.message },
{ status: 503, headers: { "retry-after": String(Math.ceil(e.retryAfterMs / 1000)) } },
),
QueryBudgetExceeded: (e) =>
HttpServerResponse.json({ error: e.message, clause: e.clause }, { status: 413 }),
// …
Unauthorized: (e) => HttpServerResponse.json({ error: e.message }, { status: 401 }),
// …
}),

To handle any error the same way, Effect.catch. In React, errorMessage(e) from ramose/react turns any of them into a string. The full list, with status codes and what the user sees, is on Errors.

You may meet these in the reference pages; none is needed to build an app.

  • Layer — wiring: how a program is given the things it needs.
  • Stream — a stream of results over time; db.live returns one, useLive consumes it for you.
  • Fiber — a running subscription; a live query is one.
  • Scope — what closes a subscription when its owner goes away.

Ramose.layer({ url, token }) is the Effect-native twin of Ramose.connect; it provides a Ramose.Databases service you yield* inside your own programs, and closes its connections when the program ends. Ramose tracks Effect 4, so the spellings are Effect.catch, Effect.catchTags, effect/Schema and Context.Service. The library’s own guide is at effect.website.