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

  1. Validate parenthesis balance before setValue(): count '(' vs ')' or use a tokenizing check.
  2. Build formulas with a tiny helper that appends and counts openers, or from a template engine that guarantees closure.
  3. 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

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


AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17). Data as JSON: /api/errors/ae8530ce1cae5609. Report an issue: GitHub.