Repository: PHPOffice/PhpSpreadsheet (php)

Issue:
Excel "CELL" function not recalculating after spreadhsheet opened by phpspreadsheets
This is:

```
- [x] a bug report
- [ ] a feature request
- [ ] **not** a usage question (ask them on https://stackoverflow.com/questions/tagged/phpspreadsheet or https://gitter.im/PHPOffice/PhpSpreadsheet)
```

### What is the expected behavior?
The Function should either retain it's original calculated value or recalculate

### What is the current behavior?
the function returns "#VALUE!"

### What are the steps to reproduce?
Use phpspreadsheet to open the file: [test.xlsx](https://github.com/PHPOffice/PhpSpreadsheet/files/8049044/test.xlsx)
this file include a cell function 
```
=CONCATENATE("Prefix ",MID(CELL("filename"),FIND("]",CELL("filename"))+1,255), " Suffix")
```
Then use phpspreadsheet to output the file; creating a new file. The newly output file will show the formula as un-calculated. (there may be some other requirements but the behavior can be observed in the included simplified example)

test.xlsx before opening: 
![file_before](https://user-images.githubusercontent.com/14942805/153602997-2d20f8a4-696d-4db8-a7bc-a9e692f66616.png)

ouput.xlsx produced by phpspreadsheet:
![file_after](https://user-images.githubusercontent.com/14942805/153603095-4fce479a-484d-4793-802e-e1f3bbaf73dc.png)


Please provide a [Minimal, Complete, and Verifiable example](https://stackoverflow.com/help/mcve) of code that exhibits the issue without relying on an external Excel file or a web server:

```php
<?php

require __DIR__ . '/vendor/autoload.php';

$sheetReader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader('Xlsx');
$sheetReader = $sheetReader->load('test.xlsx');

$sheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($sheetReader, 'Xlsx');

header('Content-Type: application/vnd.ms-excel');
header("Content-Disposition: attachment; filename=\"output.xlsx\"");
$sheetWriter->save('php://output');
```

### Which versions of PhpSpreadsheet and PHP are affected?

This was observed on php 7.4 and phpspreadsheet 1.21.0

Pull request:
Propagate Errors in Text Functions
Fix #2581 (not obvious - see next paragraph for explanation). This continues the work of PR #2902 (and also PR #3467) to have errors propagated through function calculations rather than treating them as strings. All text functions, and the concatenation operator, are addressed in this PR.

In the original issue, the spreadsheet being loaded uses the result of an unimplemented function as an argument to another function. When `getCalculatedValue` is used on the cell in question, the result is returned as `#VALUE!`. If the cell had just contained a function call to the unimplemented function, getCalculatedValue would have recognized the situation and returned oldCalculatedValue as the result. Not perfect, but good enough most of the time. User would like oldCalculatedValue returned here as well, which seems like a reasonable request.

PhpSpreadsheet always returns `#Not Yet Implemented` as the result for a function which it knows about but which is not yet implemented. That is the key to the `Cell` class being able to substitute oldCalculatedValue in the first place. However, in order to do that for the issue in question, that result has to be propagated to any functions for which the result is an argument. I don't want to add unimplemented to the list of known error codes, but I am willing to add a parameter to `ErrorValue::isError` to indicate whether that value should be considered an error (default is "no").

The first use of that new parameter would be by the text functions. They go through a common Helper routine, so it is pretty easily implemented. And, as it turns out, most of the text functions do not currently propagate errors, e.g. if A1 results in a value error, `=LEFT(A1,2)` will result in `#V` rather than `#VALUE!`. With this PR, they will now be handled correctly.

This is:

- [x] a bugfix
- [ ] a new feature
- [ ] refactoring
- [ ] additional unit tests

Checklist:

- [x] Changes are covered by unit tests
  - [x] Changes are covered by existing unit tests
  - [x] New unit tests have been added
- [x] Code style is respected
- [x] Commit message explains **why** the change is made (see https://github.com/erlang/otp/wiki/Writing-good-commit-messages)
- [ ] CHANGELOG.md contains a short summary of the change and a link to the pull request if applicable
- [ ] Documentation is updated as necessary

### Why this change is needed?

Provide an explanation of why this change is needed, with links to any Issues (if appropriate).
If this is a bugfix or a new feature, and there are no existing Issues, then please also create an issue that will make it easier to track progress with this PR.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ccb6c3af1..b0aa81f4b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
 - Mpdf and Tcpdf Borders on Merged Cells. [Issue #3557](https://github.com/PHPOffice/PhpSpreadsheet/issues/3557) [PR #4047](https://github.com/PHPOffice/PhpSpreadsheet/pull/4047)
 - Xls Conditional Format Improvements. [PR #4030](https://github.com/PHPOffice/PhpSpreadsheet/pull/4030) [PR #4033](https://github.com/PHPOffice/PhpSpreadsheet/pull/4033)
 - Conditional Range Unions and Intersections [Issue #4039](https://github.com/PHPOffice/PhpSpreadsheet/issues/4039) [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042)
+- Propagate errors in Text functions. [Issue #2581](https://github.com/PHPOffice/PhpSpreadsheet/issues/2581) [PR #4080](https://github.com/PHPOffice/PhpSpreadsheet/pull/4080)
 - Csv Reader allow use of html mimetype. [Issue #4036](https://github.com/PHPOffice/PhpSpreadsheet/issues/4036) [PR #4040](https://github.com/PHPOffice/PhpSpreadsheet/pull/4040)
 - Problem rendering line chart with missing plot label. [PR #4074](https://github.com/PHPOffice/PhpSpreadsheet/pull/4074)
 - More RTL in Xlsx/Html Comments [Issue #4004](https://github.com/PHPOffice/PhpSpreadsheet/issues/4004) [PR #4065](https://github.com/PHPOffice/PhpSpreadsheet/pull/4065)
diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php
index c5dd47310..c6f20364e 100644
--- a/src/PhpSpreadsheet/Calculation/Calculation.php
+++ b/src/PhpSpreadsheet/Calculation/Calculation.php
@@ -4792,13 +4792,20 @@ class Calculation
 
                             for ($row = 0; $row < $rows; ++$row) {
                                 for ($column = 0; $column < $columns; ++$column) {
-                                    $operand1[$row][$column]
-                                        = Shared\StringHelper::substring(
-                                            self::boolToString($operand1[$row][$column])
-                                            . self::boolToString($operand2[$row][$column]),
-                                            0,
-                                            DataType::MAX_STRING_LENGTH
-                                        );
+                                    $op1x = self::boolToString($operand1[$row][$column]);
+                                    $op2x = self::boolToString($operand2[$row][$column]);
+                                    if (Information\ErrorValue::isError($op1x)) {
+                                        // no need to do anything
+                                    } elseif (Information\ErrorValue::isError($op2x)) {
+                                        $operand1[$row][$column] = $op2x;
+                                    } else {
+                                        $operand1[$row][$column]
+                                            = Shared\StringHelper::substring(
+                                                $op1x . $op2x,
+                                                0,
+                                                DataType::MAX_STRING_LENGTH
+                                            );
+                                    }
                                 }
                             }
                             $result = $operand1;
@@ -4808,7 +4815,13 @@ class Calculation
                             // using the concatenation operator
                             // with literals that fits in 32K,
                             // so I don't think we can overflow here.
-                            $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE;
+                            if (Information\ErrorValue::isError($operand1)) {
+                                $result = $operand1;
+                            } elseif (Information\ErrorValue::isError($operand2)) {
+                                $result = $operand2;
+                            } else {
+                                $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE;
+                            }
                         }
                         $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result));
                         $stack->push('Value', $result);
diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php
index 6b7450069..77f8317ae 100644
--- a/src/PhpSpreadsheet/Calculation/Functions.php
+++ b/src/PhpSpreadsheet/Calculation/Functions.php
@@ -26,6 +26,8 @@ class Functions
     const RETURNDATE_PHP_DATETIME_OBJECT = 'O';
     const RETURNDATE_EXCEL = 'E';
 
+    public const NOT_YET_IMPLEMENTED = '#Not Yet Implemented';
+
     /**
      * Compatibility mode to use for error checking and responses.
      */
@@ -123,7 +125,7 @@ class Functions
      */
     public static function DUMMY(): string
     {
-        return '#Not Yet Implemented';
+        return self::NOT_YET_IMPLEMENTED;
     }
 
     public static function isMatrixValue(mixed $idx): bool
diff --git a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php
index dcef43990..f3a746273 100644
--- a/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php
+++ b/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php
@@ -3,6 +3,7 @@
 namespace PhpOffice\PhpSpreadsheet\Calculation\Information;
 
 use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
 
 class ErrorValue
 {
@@ -35,7 +36,7 @@ class ErrorValue
      * @return array|bool If an array of numbers is passed as an argument, then the returned result will also be an array
      *            with the same dimensions
      */
-    public static function isError(mixed $value = ''): array|bool
+    public static function isError(mixed $value = '', bool $tryNotImplemented = false): array|bool
     {
         if (is_array($value)) {
             return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
@@ -44,6 +45,9 @@ class ErrorValue
         if (!is_string($value)) {
             return false;
         }
+        if ($tryNotImplemented && $value === Functions::NOT_YET_IMPLEMENTED) {
+            return true;
+        }
 
         return in_array($value, ExcelError::ERROR_CODES, true);
     }
diff --git a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php
index 83cc4ee1f..6667bac54 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php
@@ -3,6 +3,7 @@
 namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
 
 use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
+use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
 use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
 
 class CaseConvert
@@ -26,7 +27,11 @@ class CaseConvert
             return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue);
         }
 
-        $mixedCaseValue = Helpers::extractString($mixedCaseValue);
+        try {
+            $mixedCaseValue = Helpers::extractString($mixedCaseValue, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
 
         return StringHelper::strToLower($mixedCaseValue);
     }
@@ -48,7 +53,11 @@ class CaseConvert
             return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue);
         }
 
-        $mixedCaseValue = Helpers::extractString($mixedCaseValue);
+        try {
+            $mixedCaseValue = Helpers::extractString($mixedCaseValue, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
 
         return StringHelper::strToUpper($mixedCaseValue);
     }
@@ -70,7 +79,11 @@ class CaseConvert
             return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue);
         }
 
-        $mixedCaseValue = Helpers::extractString($mixedCaseValue);
+        try {
+            $mixedCaseValue = Helpers::extractString($mixedCaseValue, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
 
         return StringHelper::strToTitle($mixedCaseValue);
     }
diff --git a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php
index 8d90a17ba..06d0f9009 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php
@@ -3,6 +3,7 @@
 namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
 
 use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
+use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
 use PhpOffice\PhpSpreadsheet\Calculation\Functions;
 use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
 
@@ -26,7 +27,12 @@ class CharacterConvert
             return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
         }
 
-        $character = Helpers::validateInt($character);
+        try {
+            $character = Helpers::validateInt($character, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
+
         $min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1;
         if ($character < $min || $character > 255) {
             return ExcelError::VALUE();
@@ -52,7 +58,12 @@ class CharacterConvert
             return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
         }
 
-        $characters = Helpers::extractString($characters);
+        try {
+            $characters = Helpers::extractString($characters, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
+
         if ($characters === '') {
             return ExcelError::VALUE();
         }
diff --git a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php
index c2281d436..78940ed16 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php
@@ -27,7 +27,7 @@ class Concatenate
 
         foreach ($aArgs as $arg) {
             $value = Helpers::extractString($arg);
-            if (ErrorValue::isError($value)) {
+            if (ErrorValue::isError($value, true)) {
                 $returnValue = $value;
 
                 break;
@@ -85,7 +85,7 @@ class Concatenate
     {
         foreach ($aArgs as $key => &$arg) {
             $value = Helpers::extractString($arg);
-            if (ErrorValue::isError($value)) {
+            if (ErrorValue::isError($value, true)) {
                 return $value;
             }
 
@@ -123,7 +123,7 @@ class Concatenate
 
         if (!is_numeric($repeatCount) || $repeatCount < 0) {
             $returnValue = ExcelError::VALUE();
-        } elseif (ErrorValue::isError($stringValue)) {
+        } elseif (ErrorValue::isError($stringValue, true)) {
             $returnValue = $stringValue;
         } else {
             $returnValue = str_repeat($stringValue, (int) $repeatCount);
diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php
index 32e5b967a..1dfb724cd 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php
@@ -31,7 +31,7 @@ class Extract
         }
 
         try {
-            $value = Helpers::extractString($value);
+            $value = Helpers::extractString($value, true);
             $chars = Helpers::extractInt($chars, 0, 1);
         } catch (CalcExp $e) {
             return $e->getMessage();
@@ -61,7 +61,7 @@ class Extract
         }
 
         try {
-            $value = Helpers::extractString($value);
+            $value = Helpers::extractString($value, true);
             $start = Helpers::extractInt($start, 1);
             $chars = Helpers::extractInt($chars, 0);
         } catch (CalcExp $e) {
@@ -90,7 +90,7 @@ class Extract
         }
 
         try {
-            $value = Helpers::extractString($value);
+            $value = Helpers::extractString($value, true);
             $chars = Helpers::extractInt($chars, 0, 1);
         } catch (CalcExp $e) {
             return $e->getMessage();
@@ -132,7 +132,13 @@ class Extract
             return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound);
         }
 
-        $text = Helpers::extractString($text ?? '');
+        try {
+            $text = Helpers::extractString($text ?? '', true);
+            Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
+
         $instance = (int) $instance;
         $matchMode = (int) $matchMode;
         $matchEnd = (int) $matchEnd;
@@ -190,7 +196,13 @@ class Extract
             return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound);
         }
 
-        $text = Helpers::extractString($text ?? '');
+        try {
+            $text = Helpers::extractString($text ?? '', true);
+            Helpers::extractString(Functions::flattenSingleValue($delimiter ?? ''), true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
+
         $instance = (int) $instance;
         $matchMode = (int) $matchMode;
         $matchEnd = (int) $matchEnd;
diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php
index 40335ced7..0560b376b 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/Format.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php
@@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
 use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
 use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
 use PhpOffice\PhpSpreadsheet\Calculation\Functions;
+use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
 use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
 use PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
 use PhpOffice\PhpSpreadsheet\RichText\RichText;
@@ -123,8 +124,13 @@ class Format
             return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format);
         }
 
-        $value = Helpers::extractString($value);
-        $format = Helpers::extractString($format);
+        try {
+            $value = Helpers::extractString($value, true);
+            $format = Helpers::extractString($format, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
+
         $format = (string) NumberFormat::convertSystemFormats($format);
 
         if (!is_numeric($value) && Date::isDateTimeFormatCode($format)) {
@@ -152,6 +158,9 @@ class Format
         }
         if (is_string($value)) {
             $value = trim($value);
+            if (ErrorValue::isError($value, true)) {
+                throw new CalcExp($value);
+            }
             if ($spacesMeanZero && $value === '') {
                 $value = 0;
             }
@@ -220,7 +229,7 @@ class Format
     }
 
     /**
-     * TEXT.
+     * VALUETOTEXT.
      *
      * @param mixed $value The value to format
      *                         Or can be an array of values
diff --git a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php
index 15b046704..719de04a8 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/Helpers.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/Helpers.php
@@ -27,7 +27,7 @@ class Helpers
         if (is_bool($value)) {
             return self::convertBooleanValue($value);
         }
-        if ($throwIfError && is_string($value) && ErrorValue::isError($value)) {
+        if ($throwIfError && is_string($value) && ErrorValue::isError($value, true)) {
             throw new CalcExp($value);
         }
 
@@ -63,18 +63,28 @@ class Helpers
             $value = (float) $value;
         }
         if (!is_numeric($value)) {
+            if (is_string($value) && ErrorValue::isError($value, true)) {
+                throw new CalcExp($value);
+            }
+
             throw new CalcExp(ExcelError::VALUE());
         }
 
         return (float) $value;
     }
 
-    public static function validateInt(mixed $value): int
+    public static function validateInt(mixed $value, bool $throwIfError = false): int
     {
         if ($value === null) {
             $value = 0;
         } elseif (is_bool($value)) {
             $value = (int) $value;
+        } elseif ($throwIfError && is_string($value) && !is_numeric($value)) {
+            if (!ErrorValue::isError($value, true)) {
+                $value = ExcelError::VALUE();
+            }
+
+            throw new CalcExp($value);
         }
 
         return (int) $value;
diff --git a/src/PhpSpreadsheet/Calculation/TextData/Search.php b/src/PhpSpreadsheet/Calculation/TextData/Search.php
index ad83f1a37..663d49fc2 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/Search.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/Search.php
@@ -32,8 +32,8 @@ class Search
         }
 
         try {
-            $needle = Helpers::extractString($needle);
-            $haystack = Helpers::extractString($haystack);
+            $needle = Helpers::extractString($needle, true);
+            $haystack = Helpers::extractString($haystack, true);
             $offset = Helpers::extractInt($offset, 1, 0, true);
         } catch (CalcExp $e) {
             return $e->getMessage();
@@ -74,8 +74,8 @@ class Search
         }
 
         try {
-            $needle = Helpers::extractString($needle);
-            $haystack = Helpers::extractString($haystack);
+            $needle = Helpers::extractString($needle, true);
+            $haystack = Helpers::extractString($haystack, true);
             $offset = Helpers::extractInt($offset, 1, 0, true);
         } catch (CalcExp $e) {
             return $e->getMessage();
diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php
index 44e0cd402..f988a6c19 100644
--- a/src/PhpSpreadsheet/Calculation/TextData/Text.php
+++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php
@@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
 
 use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
 use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
+use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
 use PhpOffice\PhpSpreadsheet\Calculation\Functions;
 use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
 
@@ -17,16 +18,20 @@ class Text
      * @param mixed $value String Value
      *                         Or can be an array of values
      *
-     * @return array|int If an array of values is passed for the argument, then the returned result
+     * @return array|int|string If an array of values is passed for the argument, then the returned result
      *            will also be an array with matching dimensions
      */
-    public static function length(mixed $value = ''): array|int
+    public static function length(mixed $value = ''): array|int|string
     {
         if (is_array($value)) {
             return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value);
         }
 
-        $value = Helpers::extractString($value);
+        try {
+            $value = Helpers::extractString($value, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
 
         return mb_strlen($value, 'UTF-8');
     }
@@ -41,17 +46,21 @@ class Text
      * @param mixed $value2 String Value
      *                         Or can be an array of values
      *
-     * @return array|bool If an array of values is passed for either of the arguments, then the returned result
+     * @return array|bool|string If an array of values is passed for either of the arguments, then the returned result
      *            will also be an array with matching dimensions
      */
-    public static function exact(mixed $value1, mixed $value2): array|bool
+    public static function exact(mixed $value1, mixed $value2): array|bool|string
     {
         if (is_array($value1) || is_array($value2)) {
             return self::evaluateArrayArguments([self::class, __FUNCTION__], $value1, $value2);
         }
 
-        $value1 = Helpers::extractString($value1);
-        $value2 = Helpers::extractString($value2);
+        try {
+            $value1 = Helpers::extractString($value1, true);
+            $value2 = Helpers::extractString($value2, true);
+        } catch (CalcExp $e) {
+            return $e->getMessage();
+        }
 
         return $value2 === $value1;
     }
@@ -97,11 +106,14 @@ class Text
      * @param mixed $padding The value with which to pad the result.
      *                              The default is #N/A.
      *
-     * @return array the array built from the text, split by the row and column delimiters
+     * @return array|string the array built from the text, split by the row and column delimiters, or an error string
      */
-    public static function split(mixed $text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, mixed $padding = '#N/A'): array
+    public static function split(mixed $text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, mixed $padding = '#N/A'): array|string
     {
         $text = Functions::flattenSingleValue($text);
+        if (ErrorValue::isError($text, true)) {
+            return $text;
+        }
 
         $flags = self::matchFlags($matchMode);
 
diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php
index 8cf2cf6c1..6aed63f20 100644
--- a/src/PhpSpreadsheet/Cell/Cell.php
+++ b/src/PhpSpreadsheet/Cell/Cell.php
@@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell;
 
 use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
 use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
+use PhpOffice\PhpSpreadsheet\Calculation\Functions;
 use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
 use PhpOffice\PhpSpreadsheet\Collection\Cells;
 use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
@@ -415,7 +416,7 @@ class Cell implements Stringable
             }
             SharedDate::setExcelCalendar($currentCalendar);
 
-            if ($result === '#Not Yet Implemented') {
+            if ($result === Functions::NOT_YET_IMPLEMENTED) {
                 return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
             }
 


Test patch:
diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php
new file mode 100644
index 000000000..9ab19d94c
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ErrorPropagationTest.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData;
+
+class ErrorPropagationTest extends AllSetupTeardown
+{
+    public function testErrorPropagation(): void
+    {
+        $sheet = $this->getSheet();
+        $sheet->getCell('A1')->setValue('=ABS("X")');
+        self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue());
+        $sheet->getCell('A2')->setValue('=SQRT(-1)');
+        self::assertSame('#NUM!', $sheet->getCell('A2')->getCalculatedValue());
+        $sheet->getCell('A3')->setValue('=3/0');
+        self::assertSame('#DIV/0!', $sheet->getCell('A3')->getCalculatedValue());
+        $sheet->getCell('A4')->setValue('=XXXX()');
+        self::assertSame('#NAME?', $sheet->getCell('A4')->getCalculatedValue());
+        $sheet->getCell('A5')->setValue('=ABS("X")');
+        self::assertSame('#VALUE!', $sheet->getCell('A5')->getCalculatedValue());
+
+        $sheet->getCell('B1')->setValue('=UPPER(A1)');
+        self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue());
+        $sheet->getCell('B2')->setValue('=LOWER(A2)');
+        self::assertSame('#NUM!', $sheet->getCell('A2')->getCalculatedValue());
+        $sheet->getCell('B3')->setValue('=PROPER(A3)');
+        self::assertSame('#DIV/0!', $sheet->getCell('A3')->getCalculatedValue());
+
+        $sheet->getCell('C2')->setValue('=CHAR(A2)');
+        self::assertSame('#NUM!', $sheet->getCell('C2')->getCalculatedValue());
+        $sheet->getCell('C3')->setValue('=CODE(A3)');
+        self::assertSame('#DIV/0!', $sheet->getCell('C3')->getCalculatedValue());
+
+        $sheet->getCell('D1')->setValue('=CONCATENATE(A1,A1)');
+        self::assertSame('#VALUE!', $sheet->getCell('D1')->getCalculatedValue());
+        $sheet->getCell('D2')->setValue('=TEXTJOIN(",",TRUE,A2,A3)');
+        self::assertSame('#NUM!', $sheet->getCell('D2')->getCalculatedValue());
+        $sheet->getCell('D3')->setValue('=REPT(A3,3)');
+        self::assertSame('#DIV/0!', $sheet->getCell('D3')->getCalculatedValue());
+        $sheet->getCell('D4')->setValue('=CONCAT(A4,A4)');
+        self::assertSame('#NAME?', $sheet->getCell('D4')->getCalculatedValue());
+        $sheet->getCell('D5')->setValue('="X"&A4');
+        self::assertSame('#NAME?', $sheet->getCell('D5')->getCalculatedValue());
+        $sheet->getCell('D6')->setValue('=A2&"X"');
+        self::assertSame('#NUM!', $sheet->getCell('D6')->getCalculatedValue());
+
+        $sheet->getCell('E1')->setValue('=LEFT(A1)');
+        self::assertSame('#VALUE!', $sheet->getCell('E1')->getCalculatedValue());
+        $sheet->getCell('E2')->setValue('=RIGHT(A2)');
+        self::assertSame('#NUM!', $sheet->getCell('E2')->getCalculatedValue());
+        $sheet->getCell('E3')->setValue('=MID(A3,2,2)');
+        self::assertSame('#DIV/0!', $sheet->getCell('E3')->getCalculatedValue());
+        $sheet->getCell('E4')->setValue('=TEXTBEFORE(A4,"M")');
+        self::assertSame('#NAME?', $sheet->getCell('E4')->getCalculatedValue());
+        $sheet->getCell('E5')->setValue('=TEXTAFTER(A5,"U")');
+        self::assertSame('#VALUE!', $sheet->getCell('E5')->getCalculatedValue());
+
+        $sheet->getCell('F1')->setValue('=VALUETOTEXT(A1)');
+        self::assertSame('#VALUE!', $sheet->getCell('F1')->getCalculatedValue());
+        $sheet->getCell('F2')->setValue('=DOLLAR(A2)');
+        self::assertSame('#NUM!', $sheet->getCell('F2')->getCalculatedValue());
+        $sheet->getCell('F3')->setValue('=FIXED(A3)');
+        self::assertSame('#DIV/0!', $sheet->getCell('E3')->getCalculatedValue());
+        $sheet->getCell('F4')->setValue('=TEXT(A4,"M")');
+        self::assertSame('#NAME?', $sheet->getCell('F4')->getCalculatedValue());
+        $sheet->getCell('F5')->setValue('=VALUE(A2)');
+        self::assertSame('#NUM!', $sheet->getCell('F5')->getCalculatedValue());
+        $sheet->getCell('F6')->setValue('=NUMBERVALUE(A3)');
+        self::assertSame('#DIV/0!', $sheet->getCell('F6')->getCalculatedValue());
+
+        $sheet->getCell('G1')->setValue('=REPLACE("oldtext",2,2,A1)');
+        self::assertSame('#VALUE!', $sheet->getCell('G1')->getCalculatedValue());
+        $sheet->getCell('G2')->setValue('=SUBSTITUTE(A2,"U","V")');
+        self::assertSame('#NUM!', $sheet->getCell('G2')->getCalculatedValue());
+
+        $sheet->getCell('H1')->setValue('=FIND(A1, "U")');
+        self::assertSame('#VALUE!', $sheet->getCell('H1')->getCalculatedValue());
+        $sheet->getCell('H2')->setValue('=SEARCH(A2,"U")');
+        self::assertSame('#NUM!', $sheet->getCell('H2')->getCalculatedValue());
+
+        $sheet->getCell('I1')->setValue('=LEN(A1)');
+        self::assertSame('#VALUE!', $sheet->getCell('I1')->getCalculatedValue());
+        $sheet->getCell('I2')->setValue('=EXACT(A2,A2)');
+        self::assertSame('#NUM!', $sheet->getCell('I2')->getCalculatedValue());
+        $sheet->getCell('I3')->setValue('=T(A3)');
+        self::assertSame('#DIV/0!', $sheet->getCell('I3')->getCalculatedValue());
+        $sheet->getCell('I4')->setValue('=TEXTSPLIT(A4,"M")');
+        self::assertSame('#NAME?', $sheet->getCell('I4')->getCalculatedValue());
+
+        $sheet->getCell('J1')->setValue('=TRIM(A1)');
+        self::assertSame('#VALUE!', $sheet->getCell('J1')->getCalculatedValue());
+        $sheet->getCell('J2')->setValue('=CLEAN(A2)');
+        self::assertSame('#NUM!', $sheet->getCell('J2')->getCalculatedValue());
+    }
+}
diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php
new file mode 100644
index 000000000..6ba87bd53
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/Issue2581Test.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
+
+use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
+use PHPUnit\Framework\TestCase;
+
+class Issue2581Test extends TestCase
+{
+    private static string $testbook = 'tests/data/Reader/XLSX/issue.2581.xlsx';
+
+    public function testIssue2581(): void
+    {
+        // CELL function (unimplemented) embedded in another function
+        $filename = self::$testbook;
+        $reader = new Xlsx();
+        $spreadsheet = $reader->load($filename);
+        $sheet = $spreadsheet->getActiveSheet();
+        self::assertSame('=CONCATENATE("Prefix ",MID(CELL("filename"),FIND("]",CELL("filename"))+1,255), " Suffix")', $sheet->getCell('B1')->getValue());
+        self::assertSame('Prefix SomeName Suffix', $sheet->getCell('B1')->getCalculatedValue());
+        $spreadsheet->disconnectWorksheets();
+    }
+}