ruvnet/ruflo · error

Event must have a valid type string

Error message

Event must have a valid type string

What it means

As the second guard in RvfEventLog.append(), the event's type field must be a non-empty string before the event is versioned and persisted. The type drives dispatch during replay, so a missing, blank, or non-string type is rejected up front. This typically means the event was constructed without a discriminator or with a renamed field.

Source

Thrown at v3/@claude-flow/shared/src/events/rvf-event-log.ts:166

    this.snapshots.clear();
    this.initialized = false;

    this.emit('shutdown');
  }

  // ===========================================================================
  // Write Operations
  // ===========================================================================

  /** Append a domain event to the log. */
  async append(event: DomainEvent): Promise<void> {
    this.ensureInitialized();

    if (!event.aggregateId || typeof event.aggregateId !== 'string') {
      throw new Error('Event must have a valid aggregateId string');
    }
    if (!event.type || typeof event.type !== 'string') {
      throw new Error('Event must have a valid type string');
    }

    // Assign next version for aggregate
    const currentVersion = this.aggregateVersions.get(event.aggregateId) ?? 0;
    const nextVersion = currentVersion + 1;
    event.version = nextVersion;

    // Persist to disk first (crash-safe ordering)
    this.appendRecord(this.config.logPath, event);

    // Update in-memory state
    this.indexEvent(event);

    this.emit('event:appended', event);

    if (nextVersion % this.config.snapshotThreshold === 0) {
      this.emit('snapshot:recommended', {
        aggregateId: event.aggregateId,

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Always set type from a shared constant or enum at the append site
  2. Check the event shape with a type guard before append() (covers aggregateId and type together)
  3. After renames, grep for old type constant names to catch stale producers

Example fix

// before
await log.append({ aggregateId: id } as DomainEvent); // throws: invalid type

// after
await log.append({ aggregateId: id, type: 'order.created' } as DomainEvent);
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof event.type !== 'string' || event.type.length === 0) {
  throw new TypeError('DomainEvent.type must be a non-empty string');
}
await log.append(event);

Type guard

function hasEventType(e: unknown): e is DomainEvent {
  return !!e && typeof e === 'object'
    && typeof (e as any).type === 'string' && (e as any).type.length > 0
    && typeof (e as any).aggregateId === 'string' && (e as any).aggregateId.length > 0;
}

Try / catch

try {
  await log.append(event);
} catch (e) {
  if (e instanceof Error && e.message.includes('valid type string')) {
    // producer sent an event without a type discriminator: fix upstream
  } else throw e;
}

Prevention

When it happens

Trigger: append({ aggregateId, payload }) with no type; type: '' or type: undefined; type passed as a symbol/number; spread construction that overwrites or drops type.

Common situations: Factory helpers defaulting type only for some event kinds; refactors renaming event type constants; deserialization dropping the field; payloads mistakenly passed as the whole event.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/0aa5e95b3ac600ca. Report an issue: GitHub.