koala73/worldmonitor · error · ConvexError

NOT_FOUND

Error message

NOT_FOUND

What it means

Thrown by the updateCompanyForOwner mutation when the target company cannot be patched. The lookup uses the by_account_companyId index scoped to the caller's logical account, so the row must belong to the authenticated owner. The guard also rejects companies whose lifecycle is 'removed' or whose required display fields (name, domicileCountry) have been purged, because a removed company is asynchronously purged and is no longer editable.

Source

Thrown at convex/companyMonitoring/companies.ts:202

  },
});

export const updateCompanyForOwner = internalMutation({
  args: {
    ownerUserId: v.string(),
    companyId: v.string(),
    patch: companyPatchValidator,
  },
  handler: async (ctx, args) => {
    const account = await requireActiveAccount(ctx, args.ownerUserId);
    const company = await ctx.db
      .query("companyMonitoringCompanies")
      .withIndex("by_account_companyId", (q) =>
        q.eq("ownerAccountId", account.logicalAccountId).eq("companyId", args.companyId),
      )
      .unique();
    if (!company || company.lifecycle === "removed" || !company.name || !company.domicileCountry) {
      throw new ConvexError("NOT_FOUND");
    }
    const patch = args.patch;
    const addClaimInputs = patch.addClaims ?? [];
    const removeClaimInputs = patch.removeClaimIds ?? [];
    if (
      addClaimInputs.length > COMPANY_MONITORING_LIMITS.maxClaimsPerCompany ||
      removeClaimInputs.length > COMPANY_MONITORING_LIMITS.maxClaimsPerCompany
    ) {
      throw new ConvexError("INVALID_COMPANY_PATCH");
    }

    const hasName = Object.prototype.hasOwnProperty.call(patch, "name");
    const hasDomicile = Object.prototype.hasOwnProperty.call(patch, "domicileCountry");
    const hasCustomerReference = Object.prototype.hasOwnProperty.call(patch, "customerReference");
    const normalizedFields = normalizeMonitoredCompanyInput({
      name: hasName ? patch.name! : company.name,
      domicileCountry: hasDomicile ? patch.domicileCountry! : company.domicileCountry,
      customerReference: hasCustomerReference

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Verify the companyId still exists and is active/paused by listing via listCompaniesForOwner before attempting the update.
  2. If the company was removed, create a new company with createCompanyForOwner instead of patching the removed row.
  3. Handle NOT_FOUND in the caller by refreshing the company list and dropping the stale reference from local state.

Example fix

// before
await ctx.runMutation(internal.companyMonitoring.companies.updateCompanyForOwner, {
  ownerUserId, companyId: staleId, patch,
});

// after
const companies = await ctx.runQuery(internal.companyMonitoring.companies.listCompaniesForOwner, { ownerUserId });
if (!companies.some((c) => c.companyId === staleId)) {
  return { status: "gone" };
}
await ctx.runMutation(internal.companyMonitoring.companies.updateCompanyForOwner, {
  ownerUserId, companyId: staleId, patch,
});
Defensive patterns

Strategy: validation

Validate before calling

const companies = await ctx.runQuery(internal.companyMonitoring.companies.listCompaniesForOwner, { ownerUserId });
const exists = companies.some((c) => c.companyId === companyId);
if (!exists) throw new UserError("Company no longer exists");
await ctx.runMutation(internal.companyMonitoring.companies.updateCompanyForOwner, { ownerUserId, companyId, patch });

Type guard

function isEditableCompany(row: { lifecycle: string; name?: string; domicileCountry?: string }): boolean {
  return row.lifecycle !== "removed" && Boolean(row.name) && Boolean(row.domicileCountry);
}

Try / catch

try {
  await updateCompanyForOwner(...);
} catch (err) {
  if (err instanceof ConvexError && err.message === "NOT_FOUND") {
    // refresh company list, drop stale id
  } else throw err;
}

Prevention

When it happens

Trigger: Calling updateCompanyForOwner with a companyId that does not exist for this account, that belongs to a different account, that was previously set to 'removed' via setCompanyStateForOwner, or whose async purge (advanceCompanyPurge) has already nulled name/domicileCountry.

Common situations: Stale companyId held in the UI after the user removed the company in another session; race between a remove action and an in-flight update; typo or copy/paste of an ID from a different account; calling update on a company mid-purge.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/f874fde33a9f914d. Report an issue: GitHub.