apify/crawlee · error · Error

Cannot set() a borrowed OwnedOrInjected value

Error message

Cannot set() a borrowed OwnedOrInjected value

What it means

set() fills an owned, not-yet-filled OwnedOrInjected slot with the crawler-built default. Borrowed (injected) slots are never replaced — the injected instance's lifecycle belongs to the caller — so calling set() on one throws.

Source

Thrown at packages/core/src/owned_or_injected.ts:92

    }

    /**
     * The resolved instance, or `undefined` when a lazily-filled owned slot hasn't been built yet. The non-throwing
     * counterpart to {@apilink OwnedOrInjected.value|`value`} — pairs naturally with `?? fallback` so callers can read
     * a possibly-empty slot without the `isPresent ? value : …` dance.
     */
    get maybeValue(): Injected | undefined {
        return this.#present ? (this.#value as Injected) : undefined;
    }

    /**
     * Fills the (owned) slot with the crawler-built default, returning it for convenience. Only valid on an owned,
     * not-yet-filled slot: borrowed instances are never replaced and an owned slot is filled exactly once (re-setting
     * would silently orphan the previous instance's lifecycle).
     */
    set(value: Owned): Owned {
        if (!this.#owned) {
            throw new Error('Cannot set() a borrowed OwnedOrInjected value');
        }

        if (this.#present) {
            throw new Error('OwnedOrInjected value is already initialized');
        }

        this.#value = value;
        this.#present = true;

        return value;
    }

    /**
     * Runs an owned-only lifecycle hook, invoked (with the value typed as the concrete `Owned`) only when the crawler
     * owns a present instance — a no-op for a borrowed instance or an owned-but-not-yet-built slot.
     */
    async ifOwned<R>(fn: (value: Owned) => R | Promise<R>): Promise<R | undefined> {
        if (!this.#owned || !this.#present) {

View on GitHub (pinned to dbe57fb09c)

Solutions

  1. Do not call set() when the user injected an instance; check `#owned`/ownership first or read the injected value instead.
  2. Skip the default-creation step when `maybeValue` already holds an injected instance.
  3. If you need a replaceable slot, construct the OwnedOrInjected as owned rather than borrowing an injected instance.

Example fix

// before
this.clientSlot.set(createDefaultClient()); // throws when user injected one
// after
if (this.clientSlot.maybeValue == null) this.clientSlot.set(createDefaultClient());
Defensive patterns

Strategy: type-guard

Validate before calling

if (slot.maybeValue != null || !slotIsOwned(slot)) {
  // skip set(): injected or already filled
} else {
  slot.set(buildDefault());
}

Try / catch

try {
  slot.set(defaultValue);
} catch (err) {
  if ((err as Error).message === 'Cannot set() a borrowed OwnedOrInjected value') {
    // user injected their own instance; use it
    return slot.value;
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling slot.set(defaultInstance) on a slot that was constructed in 'borrowed' mode because the user injected their own instance through options.

Common situations: Library-internal lazy default creation running even though the user supplied a custom instance; custom code that obtains a slot from a configured crawler and tries to overwrite the injected dependency.

Related errors


AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30). Data as JSON: /api/errors/f4bf6c71911a616f. Report an issue: GitHub.