From 07bbd58e3c446b11cb0a88c0eb37754a0353f6f6 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 8 Sep 2026 06:08:36 +0000 Subject: [PATCH 1/3] HF-131: let a consumer tell 'no message' from 'empty message' After Tasks 1-2 no engine-produced cell error is message-less, but a custom function may still return new CellError('VALUE') with no message -- the documented, still-supported contract. Both cases used to surface identically as message: ''. DetailedCellError.hasMessage (own enumerable, readonly) tells them apart: true means a message was set, even a deliberately empty one; false means none was. test/_setupFiles/matchers/cellErrorComparison.ts already exists as the single shared strip-list both toEqualError implementations import (built during the origin-mechanism work, ahead of this task) -- extended with one line rather than introducing the second shared-file layout the original plan draft described, since duplicating the consolidation this task already has would reopen the exact drift risk that shared module exists to close. Verified the strip-list entry is load-bearing empirically, not by trusting the plan's synthetic probe: removing 'hasMessage: undefined' from the list and running the full suite produces 446 failures; restoring it returns to baseline. docs/guide/custom-functions.md gains the note this field's own PR owes it, per the plan's explicit instruction not to defer a field's documentation to a later task. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 4 ++++ docs/guide/custom-functions.md | 6 ++++++ src/CellValue.ts | 11 +++++++++++ test/_setupFiles/matchers/cellErrorComparison.ts | 1 + 4 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec3c51da11..8f4e12ea23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#131](https://github.com/handsontable/hyperformula/issues/131) + ### Changed - Changed the cell errors thrown by the formula engine to always carry a message describing their cause. [#131](https://github.com/handsontable/hyperformula/issues/131) diff --git a/docs/guide/custom-functions.md b/docs/guide/custom-functions.md index 5c68d8c114..73909b27fd 100644 --- a/docs/guide/custom-functions.md +++ b/docs/guide/custom-functions.md @@ -326,6 +326,12 @@ custom error messages. Put them to good use: let your users know what caused the error and how to avoid it in the future. ::: +Every error the engine itself produces always carries a message. A custom function's +`message` argument stays optional — if you omit it, a consumer reading the resulting +`DetailedCellError` sees `message` as an empty string, the same value HyperFormula uses +for "no message". To tell the two cases apart, check `hasMessage`: `true` means a +message was set (even if it's a deliberately empty string), `false` means none was. + ### Test your function To make sure your function works correctly, add unit tests. Use a JavaScript diff --git a/src/CellValue.ts b/src/CellValue.ts index 47930aad99..f672a3f6f4 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -12,6 +12,16 @@ export class DetailedCellError { public readonly type: ErrorType public readonly message: string + /** + * Whether the underlying error carried a message at all. + * + * `message` collapses "no message" and "an empty message" into `''`; this flag + * keeps them apart for consumers that need to know whether a cause was stated. + * Errors produced by HyperFormula itself always carry one — a `false` here + * means the error came from a custom function that did not supply a message. + */ + public readonly hasMessage: boolean + constructor( error: CellError, public readonly value: string, @@ -19,6 +29,7 @@ export class DetailedCellError { ) { this.type = error.type this.message = error.message ?? '' + this.hasMessage = error.message !== undefined } public toString(): string { diff --git a/test/_setupFiles/matchers/cellErrorComparison.ts b/test/_setupFiles/matchers/cellErrorComparison.ts index 8d391e833d..6cf9e90f61 100644 --- a/test/_setupFiles/matchers/cellErrorComparison.ts +++ b/test/_setupFiles/matchers/cellErrorComparison.ts @@ -35,6 +35,7 @@ const IGNORED_IN_STRUCTURAL_COMPARE = { address: undefined, originFunction: undefined, argumentIndex: undefined, + hasMessage: undefined, } /** From a6be99991d6efcef3808434a3b48a7fff9afda08 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 14 Sep 2026 10:06:39 +0000 Subject: [PATCH 2/3] HF-131: correct what hasMessage false actually means The JSDoc and the custom-functions guide both said a false hasMessage means the error came from a custom function. An error value typed straight into a cell also has no message, and reports originFunction 'user input'. Co-Authored-By: Claude Opus 5 (1M context) --- docs/guide/custom-functions.md | 2 +- src/CellValue.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/guide/custom-functions.md b/docs/guide/custom-functions.md index 73909b27fd..0ad01c0f9f 100644 --- a/docs/guide/custom-functions.md +++ b/docs/guide/custom-functions.md @@ -326,7 +326,7 @@ custom error messages. Put them to good use: let your users know what caused the error and how to avoid it in the future. ::: -Every error the engine itself produces always carries a message. A custom function's +Every error the engine raises while evaluating a formula carries a message. A custom function's `message` argument stays optional — if you omit it, a consumer reading the resulting `DetailedCellError` sees `message` as an empty string, the same value HyperFormula uses for "no message". To tell the two cases apart, check `hasMessage`: `true` means a diff --git a/src/CellValue.ts b/src/CellValue.ts index f672a3f6f4..ffae216791 100644 --- a/src/CellValue.ts +++ b/src/CellValue.ts @@ -17,8 +17,10 @@ export class DetailedCellError { * * `message` collapses "no message" and "an empty message" into `''`; this flag * keeps them apart for consumers that need to know whether a cause was stated. - * Errors produced by HyperFormula itself always carry one — a `false` here - * means the error came from a custom function that did not supply a message. + * Errors the engine raises while evaluating a formula always carry a message. A + * `false` here means nobody stated a cause: a custom function that did not supply + * one, or an error value a user typed straight into a cell, where the cause is the + * typing itself and `originFunction` says so. */ public readonly hasMessage: boolean From a5a04ff543406321f2d34614867d51b76a367e92 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 14 Sep 2026 10:09:01 +0000 Subject: [PATCH 3/3] HF-131: link the changelog entry to the issue it closes GitHub #131 is an unrelated, closed issue; this work is tracked by #1547. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01751430de..0e4cda653c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#131](https://github.com/handsontable/hyperformula/issues/131) +- Added `DetailedCellError.hasMessage`, so a consumer can tell a cell error that never carried a message (e.g. a custom function that omitted one) apart from one with a deliberately empty message — both previously surfaced identically as `message: ''`. [#1547](https://github.com/handsontable/hyperformula/issues/1547) ### Changed