Skip to content

Yielding a tagged resource without its .make(...) layer reaches the provider with undefined props (opaque TypeError in Worker pre-create) #1054

Description

@apostolos-geyer

A tagged Effect-native resource declared as a bare tag gets both its props and its implementation from .make(props, impl). Yielding the class without that Layer in scope hands the provider news === undefined, so the failure surfaces as an opaque TypeError deep inside the provider instead of an error naming the class and the missing Layer.

For a Cloudflare Worker:

export class ApiWorker extends Cloudflare.Worker<ApiWorker, {}>()("ApiWorker") {}
export const ApiWorkerLive = ApiWorker.make({ main: import.meta.url }, Effect.succeed({}));

export default Alchemy.Stack(
  "WorkerMissingImplStack",
  { providers: Cloudflare.providers(), state: Cloudflare.state() },
  Effect.gen(function* () {
    const api = yield* ApiWorker;      // ApiWorkerLive never provided
    return { url: api.url.as<string>() };
  }),
);
[ApiWorker] pre-creating
[ApiWorker] fail
Cause([Die(TypeError: undefined is not an object (evaluating 'news.name'))])

That line is const name = yield* createWorkerName(id, news.name) in WorkerProvider.ts's precreate. Nothing points at the real mistake, which is the missing Effect.provide([ApiWorkerLive]).

Mechanism

packages/alchemy/src/Platform.ts — the tagged branch, when the class's Self tag is absent from the context:

onNone: () =>
  resource(
    id,
    props === undefined
      ? applyTransformProps(id, props)   // <- stays `undefined`
      : externalProps(),
  ),

A tagged class WITH props and no impl is the external (non-Effect-native) form, and externalProps() spreads to at least { isExternal: true }. A bare tag has neither props nor impl — both live on the un-provided .make(...) Layer — so applyTransformProps(id, undefined) returns undefined verbatim (no transformProps hook on most platforms) and the resource is registered with no props at all. cls.make normalizes the same value with transformed ?? {}; this path does not.

This is not Worker-specific: every Platform(...) consumer (Workers, Durable Objects, Containers, Workflows, AWS.Lambda.Function, AWS.ECS.Service/Task, Kubernetes.Deployment/Job, Prisma.Compute, …) reaches its provider with news === undefined in this situation. A provider tolerant of missing props deploys a silently misconfigured resource rather than crashing.

Reproduction

Hermetic, no cloud credentials, plain bun test against repo source. A synthetic Platform resource whose provider records what it receives:

const Widget: any = Platform<Widget>("Test.PlatformWidget", {
  createRuntimeContext: () => ({}) as any,
});

const providers = Provider.succeed(Widget, {
  reconcile: Effect.fn(function* ({ id, news }: any) {
    observed = { ran: true, news };
    return { name: news?.name ?? id };
  }),
  // list/diff/delete elided
});

class BareWidget extends Widget()("BareWidget") {}   // no props, no impl, no layer

const Stack = Alchemy.Stack("PlatformMissingImplStack", { providers, state },
  Effect.gen(function* () {
    const widget = yield* BareWidget;
    return { name: widget.name };
  }),
);
provider ran: true | news: undefined
deploy SUCCEEDED

The Cloudflare Worker repro above is equally credential-free — the crash happens before any API call.

Expected

Fail fast at the yield site with an error naming the class and telling the user to provide its .make(...) Layer, e.g.

MissingImplementationError: Cloudflare.Worker<ApiWorker> was yielded without its implementation.

`ApiWorker` is declared as a bare tag — no props, no inline implementation — so both come from its `.make(...)` Layer:

  export class ApiWorker extends Cloudflare.Worker<ApiWorker>()("ApiWorker") {}
  export const ApiWorkerLive = ApiWorker.make({ /* props */ }, Effect.gen(function* () { /* ... */ }));

That Layer is not in scope where `ApiWorker` is yielded. Provide it:

  Effect.gen(function* () {
    const instance = yield* ApiWorker;
  }).pipe(Effect.provide([ApiWorkerLive]))

The correct user action is documented by examples/cloudflare-worker/alchemy.run.ts, which pipes Effect.provide([WorkerTagLive, SecondaryApiLive, SandboxLive]). Only the diagnostics are wrong.

Guarding news?.name in WorkerProvider is the wrong fix — undefined props are legitimate elsewhere (the assets-only Worker has neither main nor script), and it would only move the crash to the next prop read in each of the other platforms.

Reproduced on 42625a4 (upstream main). Related but distinct: #1049 / #1050 cover a different pre-create crash (an Output-valued main in isPythonMain).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions