#3080·exceljs

xlsx.load fails with "Worksheet name already exists" for any sheet name over 31 characters — the name setter collides with itself

Author: IvesPeiCreated Sep 8, 2026Updated Sep 8, 2026

Bug

Loading a workbook that contains a worksheet whose name is longer than 31 characters always fails, even when no other sheet shares the name:

Error: Worksheet name already exists: Existing Partner Full menu Expa
    at set name (lib/doc/worksheet.js:170)
    at set model (lib/doc/worksheet.js:904)
    at lib/doc/workbook.js:211
    at XLSX.load (lib/xlsx/xlsx.js:414)

The workbook in question has twelve sheets; one is named Existing Partner Full menu Expansion (36 characters) and none of the twelve collide after truncation to 31. Related to #910 / #1809 by message only — those are about addWorksheet(); this is the load path.

Root cause

lib/doc/worksheet.js (line numbers from current master):

javascript
set name(name) {
  …
  if (this._name === name) return;                       // :142  compares the UNtruncated argumentif (name && name.length > 31) {
    console.warn(`Worksheet name ${name} exceeds 31 chars. This will be truncated`);
    name = name.substring(0, 31);
  }
  if (this._workbook._worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) {   // :172  the list includes `this`
    throw new Error(`Worksheet name already exists: ${name}`);
  }
  this._name = name;
}

During load, the setter runs twice per sheet — once from the constructor (workbook.js:204) and again from set model (worksheet.js:904). For a name ≤ 31 characters the second call hits the early-out at :142 and nothing else happens. For a longer name the first call stores the truncated 31 characters in _name, so the second call's this._name === name is false; it truncates again and reaches the duplicate check at :172 — where the worksheet is already registered in _worksheets, and finds itself.

Net effect: any sheet name over 31 characters is unloadable, collision or not. (The warning at truncation also prints twice.)

Minimal repro

javascript
const ExcelJS = require('exceljs');
const wb = new ExcelJS.Workbook();
wb.addWorksheet('Short');                                    // fine
const ws = wb.addWorksheet('A sheet name that is longer than thirty-one');   // truncates, fine
ws.name = 'A sheet name that is longer than thirty-one';     // second assignment, as `set model` does
// → Error: Worksheet name already exists: A sheet name that is longer tha

Or load any .xlsx whose xl/workbook.xml has <sheet name="…36 chars…"/>.

Suggested fix

Exclude the worksheet being named from its own duplicate check:

javascript
if (this._workbook._worksheets.find(ws => ws && ws !== this && ws.name.toLowerCase() === name.toLowerCase())) {

Verified locally against 4.4.0: the workbook loads, the name is truncated once, and two genuinely different sheets that collide after truncation still throw. (Alternatively compare the truncated name in the early-out at :142, which would also stop the double warning — but the self-collision is the actual defect, since ws.name = ws.name-style reassignment of an over-long name would throw even outside load.)

For what it's worth, openpyxl 3.1.5 opens the same file and keeps the 36-character name, warning only that "some applications may not be able to read the file".

Environment: exceljs 4.4.0, Node 24.