actualbudget/actual · error

The provided userIds must be a non-empty array.

Error message

The provided userIds must be a non-empty array.

What it means

deleteUserAccessByFileId requires userIds to be a non-empty array and throws this error otherwise. The function builds an IN (...) clause from the array, so an empty or non-array value would produce invalid SQL or a pointless transaction. The check happens before any database work.

Source

Thrown at packages/sync-server/src/services/user-service.ts:207

    const fileExists = getFileById(fileId);
    if (!userExists || !fileExists) {
      throw new Error('User or file not found');
    }
    getAccountDb().mutate(
      'INSERT INTO user_access (user_id, file_id) VALUES (?, ?)',
      [userId, fileId],
    );
  } catch (error) {
    if (error.message.includes('UNIQUE constraint')) {
      throw new Error('Access already exists');
    }
    throw new Error(`Failed to add user access: ${error.message}`);
  }
}

export function deleteUserAccessByFileId(userIds, fileId) {
  if (!Array.isArray(userIds) || userIds.length === 0) {
    throw new Error('The provided userIds must be a non-empty array.');
  }

  const CHUNK_SIZE = 999;
  let totalChanges = 0;

  try {
    getAccountDb().transaction(() => {
      for (let i = 0; i < userIds.length; i += CHUNK_SIZE) {
        const chunk = userIds.slice(i, i + CHUNK_SIZE);
        const placeholders = chunk.map(() => '?').join(',');

        const sql = `DELETE FROM user_access WHERE user_id IN (${placeholders}) AND file_id = ?`;

        const result = getAccountDb().mutate(sql, [...chunk, fileId]);
        totalChanges += result.changes;
      }
    });
  } catch (error) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Always pass an array of user ids, e.g. deleteUserAccessByFileId([userId], fileId)
  2. Check userIds.length > 0 at the call site and skip the call when nothing needs deleting
  3. Update callers still using the old single-id signature
  4. Return early (or a 204) when the batch is empty instead of treating it as an error

Example fix

// before
deleteUserAccessByFileId(userId, fileId); // throws: not an array
// after
const userIds = Array.isArray(userId) ? userId : [userId];
if (userIds.length > 0) {
  deleteUserAccessByFileId(userIds, fileId);
}
Defensive patterns

Strategy: validation

Validate before calling

function assertNonEmptyArray(value, name) {
  if (!Array.isArray(value) || value.length === 0) {
    throw new Error(`${name} must be a non-empty array`);
  }
}
assertNonEmptyArray(userIds, 'userIds');

Type guard

function isUserIdArray(value) {
  return Array.isArray(value) && value.length > 0 &&
         value.every(id => typeof id === 'string' && id.length > 0);
}

Try / catch

try {
  deleteUserAccessByFileId(userIds, fileId);
} catch (e) {
  if (e.message.includes('must be a non-empty array')) {
    logger.warn('Nothing to revoke; skipping');
    return 0;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling deleteUserAccessByFileId(userIds, fileId) with a single id string instead of an array, an empty array after filtering, undefined/null when a caller had nothing to delete, or a destructured variable that is not an array.

Common situations: Scripts passing one userId without wrapping it in an array; batch-revocation jobs computing an empty selection and still calling the function; API callers sending a single id in the request body; refactors changing the signature from (userId, fileId) to (userIds, fileId).

Understand the failure class

Background: Invalid argument type errors: "must be of type string", "expected X, got Y", and ERR_INVALID_ARG_TYPE explained — this error's family across 15 libraries.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/490b8ad577e13f39. Report an issue: GitHub.