Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Managing state
  2. asyncProcess

asyncProcess#

asyncProcess runs an async operation and tracks its status, for work that is neither a server read nor a server write.

Use it when you need to know whether something asynchronous is running: a debounced search, a share sheet, a file export, a delay, a browser API call. Not when you fetch (query) or write (mutation) — those give you caching, params reactivity and mutation wiring on top.

The common case#

typescript
import { asyncProcess, craftComputed } from '@craft-ts/core';

const { delay } =
  yield *
  asyncProcess('delay', {
    method: (successResult: string) => successResult,
    loader: async ({ params: successResult }) => {
      await new Promise((resolve) => setTimeout(resolve, 300));
      return successResult;
    },
  });

// In a tracked generator, consume the trigger with yield*.
yield * delay.method('success');

delay.status(); // 'idle' | 'loading' | 'resolved' | 'exception'
delay.isLoading();
delay.hasValue();
delay.value(); // never throws

`method` always takes exactly one parameter

Pass an object when you need several values.

Wrapping a browser API#

This is the case asyncProcess exists for — turning a promise-returning native API into something with an observable status:

typescript
const { shareContent } =
  yield *
  asyncProcess(
    'shareContent',
    {
      method: (payload: { title: string; url: string }) => payload,
      loader: function* ({ params }) {
        return (yield* BrowserNavigator.share(params)) as Promise<undefined>;
      },
    },
    ({ resource }) => ({
      isMenuOpen: craftUse(craftComputed('isMenuOpen', function* () {
        return (yield* resource.status()) === 'loading';
      })),
    }),
  );

yield * shareContent.method({ title: 'Hello AI!', url: 'https://example.com' });
yield* shareContent.isMenuOpen();

Yielding the browser API through a service — rather than touching navigator directly — is also what makes it mockable in tests. See Browser boundaries.

Triggering from an event#

Use a source$ when the process should run on an event rather than on a call — which is also where debouncing belongs:

typescript
import { on$, source$ } from '@craft-ts/core';

const searchSource = source$<string>('searchSource');

const { delayedSearch } =
  yield *
  asyncProcess('delayedSearch', {
    method: on$(searchSource, (term) => term),
    loader: async ({ params: term }) => {
      await new Promise((resolve) => setTimeout(resolve, 300));
      return term;
    },
  });

searchSource.emit('query text'); // runs automatically

delayedSearch.source; // ReadonlySource
delayedSearch.status();

Exceptions#

Split by origin, exactly like query and mutation — params for what method rejected, loader for what the operation produced:

typescript
const { loadUser } =
  yield *
  asyncProcess('loadUser', {
    method: (value: string) =>
      value.length < 3
        ? craftException(
            { _tag: 'SEARCH_TERM_TOO_SHORT' },
            { min: 3, received: value.length },
          )
        : value,
    loader: async ({ params }) =>
      params === 'blocked'
        ? craftException({ _tag: 'USER_ACCESS_FORBIDDEN' }, { id: params })
        : { id: params, name: 'John Doe' },
  });

yield * loadUser.method('ab');
loadUser.hasException(); // true
loadUser.exceptions().params?.SEARCH_TERM_TOO_SHORT;

yield * loadUser.method('blocked');
loadUser.exceptions().loader?.USER_ACCESS_FORBIDDEN;

Pitfalls#

method needs its one parameter, even when you have nothing to pass.

value() is safe to read in templates and computed signals — it returns undefined when the process has no resolved value.

Reaching for it to fetch data. If it's an HTTP read, query gives you reactive params and mutation wiring you'd otherwise rebuild by hand.

Advanced — parallel runs by identifier

identifier keeps one resource per key so several runs coexist:

typescript
const { debouncedById } =
  yield *
  asyncProcess('debouncedById', {
    method: (payload: { successResult: string; id: string }) => payload,
    identifier: ({ id }) => id,
    loader: async ({ params: { successResult } }) => {
      await new Promise((resolve) => setTimeout(resolve, 300));
      return successResult;
    },
  });

yield * debouncedById.method({ id: '1', successResult: data1 });
yield * debouncedById.method({ id: '2', successResult: data2 });

debouncedById.select('1')?.value(); // data1
debouncedById.select('2')?.value(); // data2
Advanced — yielding dependencies

method and loader are generators, and providers scopes dependencies to this process alone. A loader must not be async or return a native Promise: use yield* for asynchronous Craft operations:

typescript
const { loadProfile } =
  yield *
  asyncProcess('loadProfile', {
    providers: [provideAsyncLogger(), provideProfileGateway()],
    method: function* (userId: string) {
      yield* AsyncLogger.log(`load:${userId}`);
      return userId;
    },
    loader: function* ({ params }) {
      return yield* ProfileGateway.load(params);
    },
  });

Advanced — injectable writes

Insertion methods provide injectAsyncProcessMethodRuntimeContext(), and the process value itself is published to providePrimitiveResourceRuntimeObserver. Both expose get, set, update, and patch for wrappers, WebMCP tools, and other advanced patterns. See Anatomy of a primitive.

See Also#

  • Which primitive should I use?

  • Browser boundaries — mocking native APIs

  • Anatomy of a primitive

←queryParamsSelecting→

On this page

  • The common case
  • Wrapping a browser API
  • Triggering from an event
  • Exceptions
  • Pitfalls
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search