---
title: "Composable conventions"
description: "The shape every nuxt-backend composable follows — an options object, refs you can destructure, isLoading, isPending and a typed error, reads that never throw and actions that reject."
canonical_url: "https://nuxt-backend.dev/docs/reference/nuxt/conventions"
---
# Composable conventions

> The shape every nuxt-backend composable follows — an options object, refs you can destructure, isLoading, isPending and a typed error, reads that never throw and actions that reject.

The package's composables are shaped like [VueUse](https://vueuse.org)'s: learn the shape once and
every composable reads the same. This page is that shape. [Composables](https://nuxt-backend.dev/docs/reference/nuxt/composables)
lists each one's signature.

## Calling one

```ts
const delivery = useEmailStatus({ emailId })                          // options only
const { results } = useSearch(api.search.searchMessages, { term })     // the query it reads, then options
```

- **Options come in one object**, typed `Use<Name>Options`. A composable that reads a query you
wrote takes that query first, the way `useQuery` does, and its options second. `useAggregate`
is the exception: after the query it takes the query's own arguments, positionally, exactly as
`useQuery` does (`'skip'` included).
- **Inputs are MaybeRefOrGetter.** Pass a plain value, a ref, a computed or a getter
(`() => route.params.id`); the composable reads it reactively and follows every change.
- **An empty input pauses a read.** An `undefined`, `null`, empty or whitespace-only id, address
or term skips the query: nothing is sent, and nothing is loading.
- **A call in the old shape warns.** In development, passing a bare value, ref or getter where
the options object goes logs a warning naming the new call.

## What it returns

A plain object, typed `Use<Name>Return`, of refs and functions. Destructure it freely: every value
in it is a ref, so destructuring keeps it reactive.

<table>
<thead>
  <tr>
    <th>
      Name
    </th>
    
    <th>
      For
    </th>
    
    <th>
      Means
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        data
      </code>
      
      , or a name for what it holds (<code>
        results
      </code>
      
      , <code>
        messages
      </code>
      
      , <code>
        sessions
      </code>
      
      )
    </td>
    
    <td>
      reads
    </td>
    
    <td>
      The value. <code>
        undefined
      </code>
      
       until the first read lands, while paused, and after a failed read. <code>
        null
      </code>
      
       when the server answered that there is nothing (no such email). A list that is empty while loading says so in its own doc
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isLoading
      </code>
    </td>
    
    <td>
      reads
    </td>
    
    <td>
      <code>
        true
      </code>
      
       until the first read for the current inputs lands. <code>
        false
      </code>
      
       while paused, and once a read has failed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        isPending
      </code>
    </td>
    
    <td>
      actions
    </td>
    
    <td>
      <code>
        true
      </code>
      
       while an action runs
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        error
      </code>
    </td>
    
    <td>
      both
    </td>
    
    <td>
      <code>
        null
      </code>
      
      , or why the last read or action failed: <code>
        { code, data, message }
      </code>
    </td>
  </tr>
</tbody>
</table>

### `error`

`error` is the same `{ code, data, message }` that [`getBackendError`](https://nuxt-backend.dev/docs/data/errors) reads,
so one reader handles a composable's failure and a failure you caught yourself:

- **A refusal** carries its catalog code and fields: `unauthenticated`, `rate_limited` with
`retryAfter`, `insufficient_credits` with `balance` and `need`, … [Errors](https://nuxt-backend.dev/docs/reference/backend/errors)
lists them.
- **Anything else** (a function that crashed, which production words as "Server Error", or an
app's own `ConvexError('…')`) reads as `unavailable` with service `backend` and reason
`unexpected`. That does not mean a service is down: it means the failure carried no code. Its
`message` is the sentence the app wrote, or "Something went wrong."; never the platform's
wrapper. In development the original error is also logged to the console, and it rides along
as the value's non-enumerable `cause`.
- **message** is a sentence to show a person. Branch on `code`, never on `message`.

A composable with both a read and actions holds the last action's failure first, and the read's
when no action has failed. An action clears its own failure when the next one starts.

```vue
<script setup lang="ts">
const { sessions, revoke, isPending, error } = useSessions()
</script>

<template>
  <p v-if="error?.code === 'rate_limited'">Too many tries. Wait a moment.</p>
  <p v-else-if="error">{{ error.message }}</p>
  <button v-for="session in sessions" :key="session.id" :disabled="isPending" @click="revoke(session.id)">
    Sign out {{ describeUserAgent(session.userAgent) ?? 'an unknown device' }}
  </button>
</template>
```

## Reads never throw, actions reject

- **A read never throws.** A refused or crashed query lands in `error`, and the page keeps
rendering. Nothing to wrap in `try`.
- **An action resolves or rejects.** It resolves with its result, or rejects with the error the
call threw, as the call threw it, and mirrors it into `error`. Await it in a `try` when the next
step depends on it; otherwise let `error` show the failure.

```ts
try {
  await revoke(id)
  toast('Signed out.')
}
catch (cause) {
  // `error` already holds getBackendError(cause) ?? unavailable/unexpected.
}
```

## Step machines never reject

`useLoginFlow` and `useEmailChange` drive a form through steps (`request-code`, `verify-code`, …).
Their actions never reject: a failed step stays where it is, with the reason in `error` and a
`retryAfter` after a rate limit, so a form never needs a `try`.

## Which composables follow them

`useSearch`, `useAggregate`, `useWorkflowStatus`, `useEmailStatus`, `useSandboxInbox` and
`useSessions` follow every rule on this page. The [Composables](https://nuxt-backend.dev/docs/reference/nuxt/composables)
reference gives each other composable's shape as it is.


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
