PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Writer\Exception
')' token expected.
Error message
')' token expected.
What it means
The Xls writer's recursive-descent formula parser consumes '(' then parses a parenthesized expression, and requires the next token to be ')'. An unbalanced parenthesis in the formula string - missing closer, or an extra opener later - makes currentToken !== ')' and throws "')' token expected." with the parser positioned at the point of imbalance.
Source
Thrown at src/PhpSpreadsheet/Writer/Xls/Parser.php:1397
/**
* It parses a factor. It assumes the following rule:
* Fact -> ( Expr )
* | CellRef
* | CellRange
* | Number
* | Function.
*
* @return mixed[] The parsed ptg'd tree on success
*/
private function fact(): array
{
$currentToken = $this->currentToken;
if ($currentToken === '(') {
$this->advance(); // eat the "("
$result = $this->parenthesizedExpression();
if ($this->currentToken !== ')') {
throw new WriterException("')' token expected.");
}
$this->advance(); // eat the ")"
return $result;
}
// if it's a reference
if (Preg::isMatch('/^\$?[A-Ia-i]?[A-Za-z]\$?\d+$/', $this->currentToken)) {
$result = $this->createTree($this->currentToken, '', '');
$this->advance();
return $result;
}
if (
Preg::isMatch(
'/^'
. self::REGEX_SHEET_TITLE_UNQUOTED
. '(\:' . self::REGEX_SHEET_TITLE_UNQUOTED
. ')?\!\$?[A-Ia-i]?[A-Za-z]\$?\d+$/u',View on GitHub (pinned to 65b080eef4)
Solutions
- Validate parenthesis balance before setValue(): count '(' vs ')' or use a tokenizing check.
- Build formulas with a tiny helper that appends and counts openers, or from a template engine that guarantees closure.
- Catch WriterException at save and log the sheet/coordinate to locate the malformed cell quickly.
Example fix
// before
$sheet->getCell('B1')->setValue('=SUM(A1:A5'); // missing ')'
(new \PhpOffice\PhpSpreadsheet\Writer\Xls($spreadsheet))->save('out.xls');
// ')' token expected.
// after: validate balance before setting
$formula = '=SUM(A1:A5';
if (substr_count($formula, '(') !== substr_count($formula, ')')) {
throw new InvalidArgumentException('Unbalanced parentheses: ' . $formula);
}
$sheet->getCell('B1')->setValue($formula . ')'); Defensive patterns
Strategy: validation
Validate before calling
/** Cheap parenthesis-balance check for user-supplied formulas. */
function formulaParensBalanced(string $formula): bool
{
$depth = 0;
$inString = false;
$len = strlen($formula);
for ($i = 0; $i < $len; $i++) {
$ch = $formula[$i];
if ($ch === '"') {
$inString = !$inString;
} elseif (!$inString) {
if ($ch === '(') {
++$depth;
} elseif ($ch === ')') {
--$depth;
if ($depth < 0) {
return false;
}
}
}
}
return $depth === 0 && !$inString;
}
$value = trim($userInput);
if ($value !== '' && $value[0] === '=' && !formulaParensBalanced($value)) {
throw new InvalidArgumentException('Formula has unbalanced parentheses: ' . $value);
}
$sheet->getCell('B1')->setValue($value); Prevention
- Validate any user-supplied formula (balance check at minimum) before setValue().
- Build formulas via small composable helpers that pair each openParen with its close instead of string concatenation.
- On save failure, log sheet title + coordinate so the one malformed cell among thousands is findable.
When it happens
Trigger: Setting a cell to a malformed formula such as =SUM(A1:A5 (missing ')'), or programmatically concatenating formula fragments that drop a closing parenthesis, then saving as Xls; the parser tolerates the string in memory but must fully parse it at write time.
Common situations: User-typed formulas from an input field saved without validation; template string building ('=IF(' . $cond . ',' . $a . ',' . $b) forgetting the final ')'; Excel round-trips where a locale-specific export mangled parentheses.
Related errors
- Unknown token $token
- Unknown range separator
- Syntax error: $currentToken, lookahead: $lookAhead, current
- Cannot yet write formulae with defined names to Xls
- Unknown sheet name $ext_ref in formula
AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17).
Data as JSON: /api/errors/ae8530ce1cae5609.
Report an issue: GitHub.