Skip to content

Add error type to Promise actors #5324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
Snapshot
} from '../types.ts';

export type PromiseSnapshot<TOutput, TInput> = Snapshot<TOutput> & {
export type PromiseSnapshot<TOutput, TInput, TError = unknown> = Snapshot<
TOutput,
TError
> & {
input: TInput | undefined;
};

Expand All @@ -19,9 +22,10 @@ const XSTATE_PROMISE_REJECT = 'xstate.promise.reject';
export type PromiseActorLogic<
TOutput,
TInput = unknown,
TEmitted extends EventObject = EventObject
TEmitted extends EventObject = EventObject,
TError = unknown
> = ActorLogic<
PromiseSnapshot<TOutput, TInput>,
PromiseSnapshot<TOutput, TInput, TError>,
{ type: string; [k: string]: unknown },
TInput, // input
AnyActorSystem,
Expand Down Expand Up @@ -61,8 +65,8 @@ export type PromiseActorLogic<
*
* @see {@link fromPromise}
*/
export type PromiseActorRef<TOutput> = ActorRefFromLogic<
PromiseActorLogic<TOutput, unknown>
export type PromiseActorRef<TOutput, TError = unknown> = ActorRefFromLogic<
PromiseActorLogic<TOutput, unknown, EventObject, TError>
>;

const controllerMap = new WeakMap<AnyActorRef, AbortController>();
Expand Down Expand Up @@ -120,7 +124,8 @@ const controllerMap = new WeakMap<AnyActorRef, AbortController>();
export function fromPromise<
TOutput,
TInput = NonReducibleUnknown,
TEmitted extends EventObject = EventObject
TEmitted extends EventObject = EventObject,
TError = unknown
Copy link
Collaborator

@Andarist Andarist Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As much as I'd like to have typed errors - I don't think this is good. We absolutely can't guarantee the correctness of this type parameter. This is as good as a type cast - but it's way more dangerous because the user won't immediately realize this. In other words, it can give somebody a false sense of safety.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn’t something I expect most people to use, but as far as I know, there’s no other way to implement something like the neverthrow wrapper I created without being able to pass the error type. Ideally, this functionality would be hidden from most users while still being accessible for those who truly need it and are comfortable type casting the error type in cases where it’s known (e.g., neverthrow, effect-ts, etc.)

If you have any suggestions for alternative solutions, I'd love to hear them.

An alternative idea would be to omit the error type additions in the fromPromise function while keeping the updates in the types file. This would allow custom variants of PromiseActorLogic and PromiseSnapshot with the error type to remain compatible with the rest of xstate. However, this approach would still require adding the ErrorFrom type.

>(
promiseCreator: ({
input,
Expand All @@ -138,8 +143,8 @@ export function fromPromise<
signal: AbortSignal;
emit: (emitted: TEmitted) => void;
}) => PromiseLike<TOutput>
): PromiseActorLogic<TOutput, TInput, TEmitted> {
const logic: PromiseActorLogic<TOutput, TInput, TEmitted> = {
): PromiseActorLogic<TOutput, TInput, TEmitted, TError> {
const logic: PromiseActorLogic<TOutput, TInput, TEmitted, TError> = {
config: promiseCreator,
transition: (state, event, scope) => {
if (state.status !== 'active') {
Expand Down
25 changes: 19 additions & 6 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@ export type OutputFrom<T> =
? (TSnapshot & { status: 'done' })['output']
: never;

export type ErrorFrom<T> =
T extends ActorLogic<
infer TSnapshot,
infer _TEvent,
infer _TInput,
infer _TSystem,
infer _TEmitted
>
? (TSnapshot & { status: 'error' })['error']
: T extends ActorRef<infer TSnapshot, infer _TEvent, infer _TEmitted>
? (TSnapshot & { status: 'error' })['error']
: never;

export type ActionFunction<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
Expand Down Expand Up @@ -420,7 +433,7 @@ export interface InvokeDefinition<
| SingleOrArray<
TransitionConfig<
TContext,
ErrorActorEvent,
ErrorActorEvent<unknown>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -670,7 +683,7 @@ type DistributeActors<
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
ErrorActorEvent,
ErrorActorEvent<ErrorFrom<TSpecificActor['logic']>>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -725,7 +738,7 @@ type DistributeActors<
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
ErrorActorEvent,
ErrorActorEvent<unknown>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -818,7 +831,7 @@ export type InvokeConfig<
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
ErrorActorEvent,
ErrorActorEvent<unknown>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -2182,7 +2195,7 @@ export type AnyActorScope = ActorScope<

export type SnapshotStatus = 'active' | 'done' | 'error' | 'stopped';

export type Snapshot<TOutput> =
export type Snapshot<TOutput, TError = unknown> =
| {
status: 'active';
output: undefined;
Expand All @@ -2196,7 +2209,7 @@ export type Snapshot<TOutput> =
| {
status: 'error';
output: undefined;
error: unknown;
error: TError;
}
| {
status: 'stopped';
Expand Down