toeverything/AFFiNE · error · MemberNotFoundInSpace

member_not_found_in_space

member_not_found_in_space

Error message

Member not found in Space ${spaceId}.

What it means

Thrown by the approveMember mutation when models.workspaceUser.get(workspaceId, userId) returns no row — the user being approved has no membership record in the workspace. It fires when the role lookup fails entirely, as opposed to a member who exists but is not UnderReview (which returns true silently). Code member_not_found_in_space, type action_forbidden, HTTP 403, data.spaceId = workspaceId.

Solutions

  1. Refresh the pending-members list and confirm the user still has a membership row in this workspace before approving.
  2. If the row is gone, treat the request as already handled/cancelled — re-sync UI state instead of retrying.
  3. Double-check that userId belongs to the same workspaceId (copy-paste errors across workspaces are common).

Example fix

// before
await approveMember(ws.id, userId); // row already removed -> member_not_found_in_space

// after
const member = await getWorkspaceMember(ws.id, userId);
if (!member || member.status !== 'UnderReview') {
  await refetchPendingList(); // someone else handled it
  return;
}
await approveMember(ws.id, userId);
Defensive patterns

Strategy: try-catch

Validate before calling

// confirm the membership row is still pending before approving
const member = await getWorkspaceMember(workspaceId, userId);
if (!member || member.status !== 'UnderReview') {
  await refetchPendingList(); // handled elsewhere or withdrawn
}

Type guard

function isMemberNotFoundInSpace(e: unknown): boolean {
  const ext = (e as { extensions?: Record<string, unknown> })?.extensions;
  return String(ext?.name ?? '').toLowerCase() === 'member_not_found_in_space';
}

Try / catch

try {
  await approveMember(workspaceId, userId);
} catch (e) {
  if (isMemberNotFoundInSpace(e)) {
    await refetchPendingList(); // request already handled — sync UI, no retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling approveMember with a userId that was never a member, was already removed/revoked, or whose join request was withdrawn — e.g. two admins acting on the same request and one already handled or rejected it.

Common situations: Stale 'pending review' list in the admin UI after another admin processed the request; approving a user removed by an owner concurrently; passing a wrong userId (typo or from another workspace).

Related errors


AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18). Data as JSON: /api/errors/49ea4352a4569018. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/workspaces/resolvers/member.ts:438

        } catch (error) {
          if (backendRuntimeErrorCode(error) === 'seat_limit') {
            throw new NoMoreSeat({ spaceId: workspaceId });
          }
          throw error;
        }

        this.event.emit('workspace.members.updated', {
          workspaceId,
        });

        await this.workspaceService.sendReviewApprovedNotification(
          role.id,
          me.id
        );
      }
      return true;
    } else {
      throw new MemberNotFoundInSpace({ spaceId: workspaceId });
    }
  }

  @Mutation(() => Boolean)
  async grantMember(
    @CurrentUser() user: CurrentUser,
    @Args('workspaceId') workspaceId: string,
    @Args('userId') userId: string,
    @Args('permission', { type: () => WorkspaceRole }) newRole: WorkspaceRole
  ) {
    let role: 'member' | 'admin' | 'owner';
    switch (newRole) {
      case WorkspaceRole.External:
        return this.revokeMember(user, workspaceId, userId);
      case WorkspaceRole.Collaborator:
        role = 'member';
        break;
      case WorkspaceRole.Admin:

View on GitHub (pinned to 2af30773ae)