iOfficeAI/OfficeCLI · error · ArgumentException

A sheet named '{caseMatch.Name}' already exists. Sheet names

Error message

A sheet named '{caseMatch.Name}' already exists. Sheet names must be unique.

What it means

Thrown by ExcelHandler.AddSheet when a sheet with the requested name already exists (case-insensitive match) and none of the exception conditions apply (ifExists=use is not set, the existing sheet is not the pristine placeholder, or no claimable property was supplied). Excel requires sheet names to be unique (case-insensitive), so adding a second sheet with the same name is a hard error. The guard distinguishes a genuine duplicate-name collision from a legitimate placeholder-claim flow.

Source

Thrown at src/officecli/Handlers/Excel/ExcelHandler.Add.Cells.cs:72

            // user-created sheet (collision is a genuine error). The
            // placeholder is identifiable as: workbook holds exactly one
            // sheet, that sheet's worksheet has empty SheetData, no
            // sheetView properties beyond defaults, no tabColor — i.e.
            // a fresh `Create blank → first Add` flow.
            var caseExact = string.Equals(caseMatch.Name, name, StringComparison.Ordinal);
            var isPlaceholder = sheets.Elements<Sheet>().Count() == 1
                && IsPristineWorksheet(workbookPart, caseMatch);
            // Placeholder claim is only meaningful when the caller actually
            // supplies a sheet-level prop that would mutate the placeholder
            // (autoFilter / tabColor / hidden). Without any such prop the
            // "claim" is a true no-op and indistinguishable from a duplicate-
            // name collision — reject so callers don't see fake success.
            var hasClaimableProp = properties.ContainsKey("autoFilter")
                || properties.ContainsKey("tabColor")
                || properties.ContainsKey("hidden");
            if (!caseExact || !isPlaceholder || !hasClaimableProp)
            {
                throw new ArgumentException(
                    $"A sheet named '{caseMatch.Name}' already exists. Sheet names must be unique.");
            }

            // Placeholder claim: route any supplied autoFilter / tabColor /
            // hidden through Set so the user's intent applies — the previous
            // silent no-op branch dropped them, which is what motivated
            // rejecting duplicates outright.
            var existingPart = (WorksheetPart)workbookPart.GetPartById(caseMatch.Id!);
            var sheetMerged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            if (properties.TryGetValue("autoFilter", out var dupAf)) sheetMerged["autofilter"] = dupAf;
            if (properties.TryGetValue("tabColor", out var dupTc)) sheetMerged["tabcolor"] = dupTc;
            if (sheetMerged.Count > 0)
                SetSheetLevel(existingPart, name, sheetMerged);
            if (properties.TryGetValue("hidden", out var dupHidden) && ParseHelpers.IsTruthy(dupHidden))
                caseMatch.State = SheetStateValues.Hidden;
            return $"/sheet[@name='{name}']";
        }
        var newWorksheetPart = workbookPart.AddNewPart<WorksheetPart>();

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Set ifExists=use in the properties to claim the existing sheet as a no-op success (this is what single-sheet subtree dumps emit for replay compatibility).
  2. Choose a unique sheet name.
  3. Delete or rename the existing sheet first before adding the new one.
  4. If replaying a dump onto a workbook that already has the sheet, ensure the batch items include ifExists=use on the 'add sheet' command.

Example fix

// before: adding a sheet whose name already exists
handler.Add("/", "sheet", new { name = "Sheet1" }); // throws if Sheet1 exists

// after: claim the existing sheet as a no-op
handler.Add("/", "sheet", new { name = "Sheet1", ifExists = "use" });
Defensive patterns

Strategy: validation

Validate before calling

// Check for existing sheet name before adding
var existing = handler.GetSheetNames();
if (existing.Any(s => s.Equals(name, StringComparison.OrdinalIgnoreCase)))
{
    // Use ifExists=use to claim the existing sheet, or pick a different name
    handler.Add("/", "sheet", new { name, ifExists = "use" });
}
else
{
    handler.Add("/", "sheet", new { name });
}

Try / catch

try
{
    handler.Add("/", "sheet", new { name = sheetName });
}
catch (ArgumentException ex) when (ex.Message.Contains("already exists"))
{
    // Sheet exists — claim it as a no-op for replay-safe batch operations
    handler.Add("/", "sheet", new { name = sheetName, ifExists = "use" });
}

Prevention

When it happens

Trigger: Calling Add with type=sheet and a name that matches an existing sheet, without setting ifExists=use and without targeting the BlankDocCreator placeholder. For example: workbook already has 'Sheet1' and you Add another sheet named 'Sheet1'. Or: Add with name='Data' when 'Data' already exists with content.

Common situations: A batch replay that creates sheets which already exist in the target workbook (common when dumping from one workbook and replaying onto another that already has those sheets); a script that auto-generates sheet names without checking for collisions; a user adding a sheet whose name differs only in case from an existing one ('Data' vs 'DATA').

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/7f503a6497c6fb26. Report an issue: GitHub.