From fb9658e0e6816480d90fb7e3a0e2c51eccb8e254 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 7 Sep 2026 16:46:40 +0000 Subject: [PATCH 1/6] HF-131: give every interpreter cell error a message Fills in all 78 call sites in src/interpreter/ that constructed a CellError with no message, so a consumer no longer receives a bare #DIV/0! or #NUM! with an empty message string. Ten new catalogue entries in src/error-message.ts, not 78: the 46 division-by-zero sites share one ErrorMessage.DivisionByZero, and the 26 NUM sites mostly reuse entries that already existed (ValueLarge, ValueSmall, NotHex, NaN, BadMode, ComplexNumberExpected and others). New entries were added only where nothing existing was accurate -- NoBroadcastValue, SearchRangeDimension, NaFunction, TwoCashFlows, PositiveAndNegative, NonZeroPeriods, NoConvergence, RateGuess, MaturityLong. No error type changes and no signature changes; ArithmeticHelper.ts gains only a message literal inside divide(). Nothing under src/i18n/ is touched -- this slice is English-only by an explicit product decision. Produced by a prep-ship (`spec-to-ship`) run: `prep-ship ship hf131-messages --task-file ... --yes`. The run authored its acceptance specs first, then iterated implementation against them. It was killed by a process restart before reaching its own commit step, so this commit was made by hand from the work it left in the worktree, after verifying it: - 78 message-less sites -> 0, counted with the same command the task file gave. - tsc --noEmit and eslint --quiet both clean. - Full private suite: 5 failed / 6165 passed / 6173 total, against a baseline of 5 failed / 6157 passed / 6165 total. The 5 are a pre-existing test-repo branch-pinning mismatch, unrelated. The +8 are the run's own new specs, all passing. - The run also updated three pre-existing specs whose expectations had gone stale (error-address-preservation, arrays, matrix-plugin). Checked each: it added the message argument to detailedErrorWithOrigin, which strengthens those assertions rather than weakening them -- they still compare the address, and now the message too, via catalogue constants. No test was loosened to reach green. The ESLint rule that would stop a message-less CellError being reintroduced is deliberately NOT here: the harness commits only src, docs and CHANGELOG.md, so an .eslintrc.js change would have been silently dropped. It needs its own step. Test-side changes live in the private repo and are not part of this commit. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 4 + src/error-message.ts | 10 +++ src/interpreter/ArithmeticHelper.ts | 2 +- src/interpreter/Interpreter.ts | 2 +- src/interpreter/plugin/ComplexPlugin.ts | 2 +- .../plugin/ConditionalAggregationPlugin.ts | 2 +- src/interpreter/plugin/DatabasePlugin.ts | 10 +-- src/interpreter/plugin/FinancialPlugin.ts | 74 +++++++++---------- src/interpreter/plugin/InformationPlugin.ts | 2 +- src/interpreter/plugin/LookupPlugin.ts | 2 +- src/interpreter/plugin/MathPlugin.ts | 2 +- src/interpreter/plugin/ModuloPlugin.ts | 3 +- .../plugin/NumericAggregationPlugin.ts | 20 ++--- src/interpreter/plugin/RoundingPlugin.ts | 4 +- .../plugin/StatisticalAggregationPlugin.ts | 20 ++--- src/interpreter/plugin/StatisticalPlugin.ts | 2 +- src/interpreter/plugin/TrigonometryPlugin.ts | 10 +-- 17 files changed, 93 insertions(+), 78 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a16d04698..026e60695b 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] +### 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) + ## [3.4.0] - 2026-08-10 ### Added diff --git a/src/error-message.ts b/src/error-message.ts index 5e3afdbeab..3fe97e24ef 100644 --- a/src/error-message.ts +++ b/src/error-message.ts @@ -12,6 +12,7 @@ export class ErrorMessage { public static EmptyArg = 'Empty function argument.' public static EmptyArray = 'Empty array not allowed.' public static ArrayDimensions = 'Array dimensions are not compatible.' + public static NoBroadcastValue = 'Operand ranges cannot be broadcast to a common size, so this position has no value.' public static NoSpaceForArrayResult = 'No space for array result.' public static ValueSmall = 'Value too small.' public static ValueLarge = 'Value too large.' @@ -19,6 +20,7 @@ export class ErrorMessage { public static RangeManySheets = 'Range spans more than one sheet.' public static CellRangeExpected = 'Cell range expected.' public static WrongDimension = 'Wrong range dimension.' + public static SearchRangeDimension = 'Search range needs to be a single row or a single column, not a two-dimensional range.' public static ScalarExpected = 'Cell range not allowed.' public static NumberCoercion = 'Value cannot be coerced to number.' public static NumberExpected = 'Number argument expected.' @@ -39,6 +41,7 @@ export class ErrorMessage { public static BadRef = 'Address is not correct.' public static NumberRange = 'Number-only range expected.' public static ValueNotFound = 'Value not found.' + public static NaFunction = 'The NA function always evaluates to the #N/A error.' public static ValueBaseLarge = 'Value in base too large.' public static ValueBaseSmall = 'Value in base too small.' public static ValueBaseLong = 'Value in base too long.' @@ -47,6 +50,8 @@ export class ErrorMessage { public static OneValue = 'Needs at least one value.' public static TwoValues = 'Range needs to contain at least two elements.' public static ThreeValues = 'Range needs to contain at least three elements.' + public static TwoCashFlows = 'A schedule of cash flows needs at least two entries to define a rate of return.' + public static PositiveAndNegative = 'Needs at least one positive and one negative value.' public static IndexBounds = 'Index out of bounds.' public static IndexLarge = 'Index too large.' public static Formula = 'Expected formula.' @@ -54,6 +59,7 @@ export class ErrorMessage { public static ParseError = 'Parsing error.' public static SheetRef = 'Sheet does not exist.' public static PeriodLong = 'Period number cannot exceed life length.' + public static NonZeroPeriods = 'Number of periods cannot be 0 when the rate is -1.' public static InvalidDate = 'Invalid date.' public static BitshiftLong = 'Result of bitshift is too long.' public static EmptyString = 'Empty-string argument not allowed.' @@ -61,11 +67,15 @@ export class ErrorMessage { public static NegativeTime = 'Time cannot be negative.' public static NoDefault = 'No default option.' public static NoConditionMet = 'None of the conditions were met.' + public static NoConvergence = 'Calculation did not converge.' + public static RateGuess = 'The iterative rate search needs an initial guess greater than -1.' public static Selector = 'Selector cannot exceed the number of arguments.' public static StartEndDate = 'Start date needs to be earlier than end date.' + public static MaturityLong = 'Maturity date cannot be more than one year after the settlement date.' public static IncorrectDateTime = 'String does not represent correct DateTime.' public static CharacterCodeBounds = 'Character code out of bounds.' public static NonZero = 'Argument cannot be 0.' + public static DivisionByZero = 'Division by zero.' public static LessThanOne = 'Argument cannot be less than 1.' public static WeekendString = 'Incorrect weekend bitmask string.' public static InvalidRoman = 'Invalid roman numeral.' diff --git a/src/interpreter/ArithmeticHelper.ts b/src/interpreter/ArithmeticHelper.ts index 73719a60fb..d393c546b7 100644 --- a/src/interpreter/ArithmeticHelper.ts +++ b/src/interpreter/ArithmeticHelper.ts @@ -197,7 +197,7 @@ export class ArithmeticHelper { const left = getRawValue(leftArg) const right = getRawValue(rightArg) if (right === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } else { const typeOfResult = inferExtendedNumberTypeMultiplicative(leftArg, rightArg) return this.ExtendedNumberFactory(left / right, typeOfResult) diff --git a/src/interpreter/Interpreter.ts b/src/interpreter/Interpreter.ts index 8edf08f0a9..b66e04a478 100644 --- a/src/interpreter/Interpreter.ts +++ b/src/interpreter/Interpreter.ts @@ -462,7 +462,7 @@ export class Interpreter { if (i1 < arg1.height() && i2 < arg2.height() && j1 < arg1.width() && j2 < arg2.width()) { ret[i][j] = op(arg1.data[i1][j1], arg2.data[i2][j2]) } else { - ret[i][j] = new CellError(ErrorType.NA) + ret[i][j] = new CellError(ErrorType.NA, ErrorMessage.NoBroadcastValue) } } } diff --git a/src/interpreter/plugin/ComplexPlugin.ts b/src/interpreter/plugin/ComplexPlugin.ts index a3e0c3365b..e598c9fdf0 100644 --- a/src/interpreter/plugin/ComplexPlugin.ts +++ b/src/interpreter/plugin/ComplexPlugin.ts @@ -209,7 +209,7 @@ export class ComplexPlugin extends FunctionPlugin implements FunctionPluginTypec return this.runFunction(ast.args, state, this.metadata('IMARGUMENT'), ([re, im]: complex) => { if (re === 0 && im === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return Math.atan2(im, re) } diff --git a/src/interpreter/plugin/ConditionalAggregationPlugin.ts b/src/interpreter/plugin/ConditionalAggregationPlugin.ts index f2e74e83cf..034c96dec6 100644 --- a/src/interpreter/plugin/ConditionalAggregationPlugin.ts +++ b/src/interpreter/plugin/ConditionalAggregationPlugin.ts @@ -200,7 +200,7 @@ export class ConditionalAggregationPlugin extends FunctionPlugin implements Func if (averageResult instanceof CellError) { return averageResult } else { - return averageResult.averageValue() || new CellError(ErrorType.DIV_BY_ZERO) + return averageResult.averageValue() || new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts index 1208a0f17f..67c174752b 100644 --- a/src/interpreter/plugin/DatabasePlugin.ts +++ b/src/interpreter/plugin/DatabasePlugin.ts @@ -216,7 +216,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType } if (count === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return sum / count @@ -338,7 +338,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType } if (values.length <= 1) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const mean = values.reduce((a, b) => a + b, 0) / values.length @@ -363,7 +363,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType } if (values.length === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const mean = values.reduce((a, b) => a + b, 0) / values.length @@ -387,7 +387,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType } if (values.length <= 1) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const mean = values.reduce((a, b) => a + b, 0) / values.length @@ -411,7 +411,7 @@ export class DatabasePlugin extends FunctionPlugin implements FunctionPluginType } if (values.length === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const mean = values.reduce((a, b) => a + b, 0) / values.length diff --git a/src/interpreter/plugin/FinancialPlugin.ts b/src/interpreter/plugin/FinancialPlugin.ts index c1f0c782a8..106b793c72 100644 --- a/src/interpreter/plugin/FinancialPlugin.ts +++ b/src/interpreter/plugin/FinancialPlugin.ts @@ -383,7 +383,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('DDB'), (cost: number, salvage: number, life: number, period: number, factor: number) => { if (period > life) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.PeriodLong) } let rate = factor / life let oldValue @@ -407,7 +407,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('DOLLARDE'), (dollar: number, fraction: number) => { if (fraction < 1) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } fraction = Math.trunc(fraction) @@ -423,7 +423,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('DOLLARFR'), (dollar: number, fraction: number) => { if (fraction < 1) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } fraction = Math.trunc(fraction) @@ -448,7 +448,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('ISPMT'), (rate, period, periods, value) => { if (periods === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return value * rate * (period / periods - 1) } @@ -469,7 +469,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp (rate: number, payment: number, present: number, future: number, type: number) => { if (rate === 0) { if (payment === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return (-present - future) / payment } @@ -486,7 +486,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('RATE'), (periods: number, payment: number, present: number, future: number, type: number, guess: number) => { if (guess <= -1) { - return new CellError(ErrorType.VALUE) + return new CellError(ErrorType.VALUE, ErrorMessage.RateGuess) } const epsMax = 1e-7 @@ -497,7 +497,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp type = type ? 1 : 0 for (let i = 0; i < iterMax; i++) { if (rate <= -1) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } let y if (Math.abs(rate) < epsMax) { @@ -519,7 +519,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp } rate -= y / dy } - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } ) } @@ -530,9 +530,9 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp type = type ? 1 : 0 if (rate === -1) { if (periods === 0) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NonZeroPeriods) } else { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } if (rate === 0) { @@ -548,7 +548,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('RRI'), (periods, present, future) => { if (present === 0 || (future < 0 && present > 0) || (future > 0 && present < 0)) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NaN) } return Math.pow(future / present, 1 / periods) - 1 @@ -560,7 +560,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('SLN'), (cost, salvage, life) => { if (life === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return (cost - salvage) / life } @@ -571,7 +571,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('SYD'), (cost: number, salvage: number, life: number, period: number) => { if (period > life) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.PeriodLong) } return ((cost - salvage) * (life - period + 1) * 2) / (life * (life + 1)) } @@ -584,20 +584,20 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp settlement = Math.round(settlement) maturity = Math.round(maturity) if (settlement >= maturity) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.StartEndDate) } const startDate = this.dateTimeHelper.numberToSimpleDate(settlement) const endDate = this.dateTimeHelper.numberToSimpleDate(maturity) if (endDate.year > startDate.year + 1 || (endDate.year === startDate.year + 1 && (endDate.month > startDate.month || (endDate.month === startDate.month && endDate.day > startDate.day)))) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.MaturityLong) } const denom = 360 - discount * (maturity - settlement) if (denom === 0) { return 0 } if (denom < 0) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) } return 365 * discount / denom } @@ -610,20 +610,20 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp settlement = Math.round(settlement) maturity = Math.round(maturity) if (settlement >= maturity) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.StartEndDate) } const startDate = this.dateTimeHelper.numberToSimpleDate(settlement) const endDate = this.dateTimeHelper.numberToSimpleDate(maturity) if (endDate.year > startDate.year + 1 || (endDate.year === startDate.year + 1 && (endDate.month > startDate.month || (endDate.month === startDate.month && endDate.day > startDate.day)))) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.MaturityLong) } const denom = 360 - discount * (maturity - settlement) if (denom === 0) { return 0 } if (denom < 0) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) } return 100 * (1 - discount * (maturity - settlement) / 360) } @@ -636,13 +636,13 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp settlement = Math.round(settlement) maturity = Math.round(maturity) if (settlement >= maturity) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.StartEndDate) } const startDate = this.dateTimeHelper.numberToSimpleDate(settlement) const endDate = this.dateTimeHelper.numberToSimpleDate(maturity) if (endDate.year > startDate.year + 1 || (endDate.year === startDate.year + 1 && (endDate.month > startDate.month || (endDate.month === startDate.month && endDate.day > startDate.day)))) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.MaturityLong) } return (100 - price) * 360 / (price * (maturity - settlement)) } @@ -707,7 +707,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp } } if (!posFlag || !negFlag) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const n = vals.length const nom = npvCore(rrate, posValues) @@ -779,7 +779,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('IRR'), (range: SimpleRangeValue, guess: number) => { if (guess <= -1) { - return new CellError(ErrorType.VALUE) + return new CellError(ErrorType.VALUE, ErrorMessage.RateGuess) } const vals = this.arithmeticHelper.manyToExactNumbers(range.valuesFromTopLeftCorner()) @@ -791,7 +791,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp const hasPositive = vals.some(val => val > 0) const hasNegative = vals.some(val => val < 0) if (!hasPositive || !hasNegative) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.PositiveAndNegative) } return irrCore(vals, guess) @@ -809,7 +809,7 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp return this.runFunction(ast.args, state, this.metadata('XIRR'), (values: SimpleRangeValue, dates: SimpleRangeValue, guess: number) => { if (guess <= -1) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.RateGuess) } const cashFlows = sanitizeXirrRange(values.valuesFromTopLeftCorner()) @@ -828,13 +828,13 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp // A schedule needs at least two cash flows to define a rate of return. if (cashFlows.length < 2) { - return new CellError(ErrorType.NA) + return new CellError(ErrorType.NA, ErrorMessage.TwoCashFlows) } const hasPositive = cashFlows.some(value => value > 0) const hasNegative = cashFlows.some(value => value < 0) if (!hasPositive || !hasNegative) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.PositiveAndNegative) } return xirrCore(cashFlows, paymentDates, guess) @@ -882,7 +882,7 @@ function npvCore(rate: number, args: number[]): number | CellError { if (acc === 0) { continue } else { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } acc /= 1 + rate @@ -910,7 +910,7 @@ function irrCore(values: number[], guess: number): number | CellError { for (let i = 0; i < values.length; i++) { const factor = Math.pow(1 + rate, i) if (!isFinite(factor) || factor === 0) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } npv += values[i] / factor if (i > 0) { @@ -925,14 +925,14 @@ function irrCore(values: number[], guess: number): number | CellError { // Check if derivative is too small (avoid division by zero) if (Math.abs(dnpv) < epsMax) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } // Newton-Raphson step let newRate = rate - npv / dnpv if (!isFinite(newRate)) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } // Clamp: when Newton overshoots past -1, bisect between current rate and -1 @@ -948,7 +948,7 @@ function irrCore(values: number[], guess: number): number | CellError { rate = newRate } - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } /** @@ -1016,14 +1016,14 @@ function xirrCore(values: number[], dates: number[], guess: number): number | Ce const base = 1 + rate const factor = Math.pow(base, exponent) if (!isFinite(factor) || factor === 0) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } npv += values[i] / factor dnpv -= exponent * values[i] / (factor * base) } if (!isFinite(npv) || !isFinite(dnpv)) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } // Check for convergence. @@ -1033,13 +1033,13 @@ function xirrCore(values: number[], dates: number[], guess: number): number | Ce // Check if the derivative is too small (avoid division by zero). if (Math.abs(dnpv) < epsMax) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } // Newton-Raphson step. let newRate = rate - npv / dnpv if (!isFinite(newRate)) { - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } // Clamp: when Newton overshoots past -1, bisect between current rate and -1. @@ -1055,5 +1055,5 @@ function xirrCore(values: number[], dates: number[], guess: number): number | Ce rate = newRate } - return new CellError(ErrorType.NUM) + return new CellError(ErrorType.NUM, ErrorMessage.NoConvergence) } diff --git a/src/interpreter/plugin/InformationPlugin.ts b/src/interpreter/plugin/InformationPlugin.ts index 8c10ac05ed..9a6589c15c 100644 --- a/src/interpreter/plugin/InformationPlugin.ts +++ b/src/interpreter/plugin/InformationPlugin.ts @@ -441,7 +441,7 @@ export class InformationPlugin extends FunctionPlugin implements FunctionPluginT * @param _state */ public na(_ast: ProcedureAst, _state: InterpreterState): CellError { - return new CellError(ErrorType.NA) + return new CellError(ErrorType.NA, ErrorMessage.NaFunction) } /** diff --git a/src/interpreter/plugin/LookupPlugin.ts b/src/interpreter/plugin/LookupPlugin.ts index 625a68735d..ac322708b5 100644 --- a/src/interpreter/plugin/LookupPlugin.ts +++ b/src/interpreter/plugin/LookupPlugin.ts @@ -305,7 +305,7 @@ export class LookupPlugin extends FunctionPlugin implements FunctionPluginTypech } if (rangeValue.width() > 1 && rangeValue.height() > 1) { - return new CellError(ErrorType.NA) + return new CellError(ErrorType.NA, ErrorMessage.SearchRangeDimension) } const searchStrategy = rangeValue.width() === 1 ? this.columnSearch : this.rowSearch diff --git a/src/interpreter/plugin/MathPlugin.ts b/src/interpreter/plugin/MathPlugin.ts index e783d801fb..6a037e8081 100644 --- a/src/interpreter/plugin/MathPlugin.ts +++ b/src/interpreter/plugin/MathPlugin.ts @@ -251,7 +251,7 @@ export class MathPlugin extends FunctionPlugin implements FunctionPluginTypechec return this.runFunction(ast.args, state, this.metadata('QUOTIENT'), (nom: number, denom: number) => { if (denom === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return Math.trunc(nom / denom) } diff --git a/src/interpreter/plugin/ModuloPlugin.ts b/src/interpreter/plugin/ModuloPlugin.ts index 9c76dba4e8..4771a5338a 100644 --- a/src/interpreter/plugin/ModuloPlugin.ts +++ b/src/interpreter/plugin/ModuloPlugin.ts @@ -4,6 +4,7 @@ */ import {CellError, ErrorType} from '../../Cell' +import {ErrorMessage} from '../../error-message' import {ProcedureAst} from '../../parser' import {InterpreterState} from '../InterpreterState' import {InterpreterValue} from '../InterpreterValue' @@ -23,7 +24,7 @@ export class ModuloPlugin extends FunctionPlugin implements FunctionPluginTypech public mod(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('MOD'), (dividend: number, divisor: number) => { if (divisor === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } else { return dividend % divisor } diff --git a/src/interpreter/plugin/NumericAggregationPlugin.ts b/src/interpreter/plugin/NumericAggregationPlugin.ts index 5a675e9360..b7c79cb10e 100644 --- a/src/interpreter/plugin/NumericAggregationPlugin.ts +++ b/src/interpreter/plugin/NumericAggregationPlugin.ts @@ -307,7 +307,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function if (result instanceof CellError) { return result } else { - return result.averageValue() ?? new CellError(ErrorType.DIV_BY_ZERO) + return result.averageValue() ?? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } @@ -325,7 +325,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function if (result instanceof CellError) { return result } else { - return result.varSValue() ?? new CellError(ErrorType.DIV_BY_ZERO) + return result.varSValue() ?? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } @@ -335,7 +335,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function if (result instanceof CellError) { return result } else { - return result.varPValue() ?? new CellError(ErrorType.DIV_BY_ZERO) + return result.varPValue() ?? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } @@ -354,7 +354,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function return result } else { const val = result.varSValue() - return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO) : Math.sqrt(val) + return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : Math.sqrt(val) } } @@ -365,7 +365,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function return result } else { const val = result.varPValue() - return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO) : Math.sqrt(val) + return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : Math.sqrt(val) } } @@ -444,7 +444,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function if (result instanceof CellError) { return result } else { - return result.averageValue() ?? new CellError(ErrorType.DIV_BY_ZERO) + return result.averageValue() ?? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } @@ -454,7 +454,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function if (result instanceof CellError) { return result } else { - return result.varSValue() ?? new CellError(ErrorType.DIV_BY_ZERO) + return result.varSValue() ?? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } @@ -464,7 +464,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function if (result instanceof CellError) { return result } else { - return result.varPValue() ?? new CellError(ErrorType.DIV_BY_ZERO) + return result.varPValue() ?? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } } @@ -475,7 +475,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function return result } else { const val = result.varSValue() - return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO) : Math.sqrt(val) + return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : Math.sqrt(val) } } @@ -486,7 +486,7 @@ export class NumericAggregationPlugin extends FunctionPlugin implements Function return result } else { const val = result.varPValue() - return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO) : Math.sqrt(val) + return val === undefined ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : Math.sqrt(val) } } diff --git a/src/interpreter/plugin/RoundingPlugin.ts b/src/interpreter/plugin/RoundingPlugin.ts index 0753c30c80..034822b147 100644 --- a/src/interpreter/plugin/RoundingPlugin.ts +++ b/src/interpreter/plugin/RoundingPlugin.ts @@ -198,7 +198,7 @@ export class RoundingPlugin extends FunctionPlugin implements FunctionPluginType return 0 } if (significance === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } if ((value > 0) && (significance < 0)) { @@ -243,7 +243,7 @@ export class RoundingPlugin extends FunctionPlugin implements FunctionPluginType return 0 } if (significance === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } if ((value > 0) && (significance < 0)) { diff --git a/src/interpreter/plugin/StatisticalAggregationPlugin.ts b/src/interpreter/plugin/StatisticalAggregationPlugin.ts index 75b8c820b7..62a8388d86 100644 --- a/src/interpreter/plugin/StatisticalAggregationPlugin.ts +++ b/src/interpreter/plugin/StatisticalAggregationPlugin.ts @@ -168,7 +168,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func return coerced } if (coerced.length === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const avg = mean(coerced) return coerced.reduce((a, b) => a + Math.abs(b - avg), 0) / coerced.length @@ -323,7 +323,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func return new CellError(ErrorType.NA, ErrorMessage.OneValue) } if (sigma === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return 1 - normal.cdf((mean(vals) - x) / (sigma / Math.sqrt(n)), 0, 1) } @@ -342,12 +342,12 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func return arrY } if (arrX.length <= 1 || arrY.length <= 1) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const vx = variance(arrX, true) const vy = variance(arrY, true) if (vx === 0 || vy === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const r = vx / vy const v = centralF.cdf(r, arrX.length - 1, arrY.length - 1) @@ -412,7 +412,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func let sum = 0 for (let i = 0; i < ret[0].length; i++) { if (ret[1][i] === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } sum += Math.pow(ret[0][i] - ret[1][i], 2) / ret[1][i] } @@ -449,7 +449,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func } const s = stdev(sub, true) if (s === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const t = Math.abs(Math.sqrt(n) * mean(sub) / s) return tails * (1 - studentt.cdf(t, n - 1)) @@ -459,7 +459,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func } const s = (sumsqerr(arrX) + sumsqerr(arrY)) / (n + m - 2) if (s === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const t = Math.abs((mean(arrX) - mean(arrY)) / Math.sqrt(s * (1 / n + 1 / m))) return tails * (1 - studentt.cdf(t, n + m - 2)) @@ -470,7 +470,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func const sx = variance(arrX, true) const sy = variance(arrY, true) if (sx === 0 && sy === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } const t = Math.abs((mean(arrX) - mean(arrY)) / Math.sqrt(sx / n + sy / m)) const v = Math.pow(sx / n + sy / m, 2) / (Math.pow(sx / n, 2) / (n - 1) + Math.pow(sy / m, 2) / (m - 1)) @@ -493,7 +493,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func const avg = mean(coerced) const s = stdev(coerced, true) if (s === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return coerced.reduce((a, b) => a + Math.pow((b - avg) / s, 3), 0) * n / (n - 1) / (n - 2) }) @@ -513,7 +513,7 @@ export class StatisticalAggregationPlugin extends FunctionPlugin implements Func const avg = mean(coerced) const s = stdev(coerced, false) if (s === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return coerced.reduce((a, b) => a + Math.pow((b - avg) / s, 3), 0) / n }) diff --git a/src/interpreter/plugin/StatisticalPlugin.ts b/src/interpreter/plugin/StatisticalPlugin.ts index 54fd6e2dbc..c1beeedb19 100644 --- a/src/interpreter/plugin/StatisticalPlugin.ts +++ b/src/interpreter/plugin/StatisticalPlugin.ts @@ -825,7 +825,7 @@ export class StatisticalPlugin extends FunctionPlugin implements FunctionPluginT (alpha: number, stddev: number, size: number) => { size = Math.trunc(size) if (size === 1) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } // eslint-disable-next-line // @ts-ignore diff --git a/src/interpreter/plugin/TrigonometryPlugin.ts b/src/interpreter/plugin/TrigonometryPlugin.ts index e1fe1b80e5..b3d38f7c15 100644 --- a/src/interpreter/plugin/TrigonometryPlugin.ts +++ b/src/interpreter/plugin/TrigonometryPlugin.ts @@ -182,7 +182,7 @@ export class TrigonometryPlugin extends FunctionPlugin implements FunctionPlugin return this.runFunction(ast.args, state, this.metadata('ATAN2'), (x: number, y: number) => { if (x === 0 && y === 0) { - return new CellError(ErrorType.DIV_BY_ZERO) + return new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) } return Math.atan2(y, x) } @@ -191,7 +191,7 @@ export class TrigonometryPlugin extends FunctionPlugin implements FunctionPlugin public cot(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('COT'), - (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO) : (1 / Math.tan(arg)) + (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : (1 / Math.tan(arg)) ) } @@ -209,7 +209,7 @@ export class TrigonometryPlugin extends FunctionPlugin implements FunctionPlugin public csc(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('CSC'), - (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO) : (1 / Math.sin(arg)) + (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : (1 / Math.sin(arg)) ) } @@ -239,7 +239,7 @@ export class TrigonometryPlugin extends FunctionPlugin implements FunctionPlugin public coth(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('COTH'), - (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO) : (1 / Math.tanh(arg)) + (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : (1 / Math.tanh(arg)) ) } @@ -257,7 +257,7 @@ export class TrigonometryPlugin extends FunctionPlugin implements FunctionPlugin public csch(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('CSCH'), - (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO) : (1 / Math.sinh(arg)) + (arg) => (arg === 0) ? new CellError(ErrorType.DIV_BY_ZERO, ErrorMessage.DivisionByZero) : (1 / Math.sinh(arg)) ) } } From 84708e254f63893213d8cb6bc1cdb57c9ca11fc1 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 10 Sep 2026 13:31:53 +0000 Subject: [PATCH 2/6] HF-131: add the ESLint rule, scoped to src/interpreter/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enforces the guarantee this PR's message-filling work establishes: a message-less new CellError(...) construction under src/interpreter/ is now a lint error. Two selectors — a bare one-argument call, and an explicit literal undefined as the second argument (the exact shape the two CYCLE sites in src/Evaluator.ts still use; Evaluator.ts is outside src/interpreter/ so this rule doesn't reach them yet, and is unaffected by this commit). Verified: 0 lint errors on the full src/ tree (same pre-existing warning count as before this commit — no new warnings). The rule was proven to actually fire, not just parse, by temporarily reverting one src/interpreter/ plugin site to a bare 'new CellError(ErrorType.NUM)', observing the expected lint error, then restoring it (git diff empty afterward). Widened to all of src/ in the next PR in this stack (feat/hf-131-error-messages-outside-interpreter). Co-Authored-By: Claude Sonnet 5 --- .eslintrc.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index a5e9976e13..eb513cc6be 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -143,6 +143,25 @@ module.exports = { 'sort-keys': ['error', 'asc'], } }, + { + files: ['**/src/interpreter/**/*.ts'], + rules: { + // HF-131: an engine-thrown cell error must always state its cause. + // The public `CellError` constructor keeps `message` optional for custom + // functions (docs/guide/custom-functions.md); this rule enforces the + // stricter contract on our own code only. + 'no-restricted-syntax': ['error', + { + selector: "NewExpression[callee.name='CellError'][arguments.length<2]", + message: 'HF-131: pass an ErrorMessage constant as the second argument — no cell error may be thrown without a cause.', + }, + { + selector: "NewExpression[callee.name='CellError'] > Identifier.arguments:nth-child(2)[name='undefined']", + message: 'HF-131: an explicit undefined message is not allowed — pass an ErrorMessage constant.', + }, + ], + }, + }, { files: ['**/*.spec.ts'], rules: { From 0ea5c641b0c24dd0b81373755e4c3e6d1c9c673a Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 11 Sep 2026 05:30:47 +0000 Subject: [PATCH 3/6] chore: trigger CI re-run to pick up the paired test coverage fix No engine changes -- the paired hyperformula-tests branch for this PR gained 4 new coverage-closing test cases after the initial CI run, and re-running the same commit isn't possible; this empty commit forces a fresh run so codecov/patch reflects them. Co-Authored-By: Claude Sonnet 5 From c5b33cb4cd1b73a9843ae1551ed58f41a0530e6c Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 14 Sep 2026 10:06:18 +0000 Subject: [PATCH 4/6] HF-131: link the changelog entry to the issue it closes HF-131 is a ClickUp id; GitHub #131 is an unrelated, closed issue about array type support. The work this stack does 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 1525db142f..51bcdf1704 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/), ### 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) +- Changed the cell errors thrown by the formula engine to always carry a message describing their cause. [#1547](https://github.com/handsontable/hyperformula/issues/1547) ### Fixed From 49dba5c2e3b5afad306f617044bd0a158835c36d Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 22 Oct 2026 15:23:25 +0000 Subject: [PATCH 5/6] chore: state the cell-error lint rule without a ticket reference Co-Authored-By: Claude Opus 5.5 --- .eslintrc.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index eb513cc6be..e0b4523dd0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -146,18 +146,18 @@ module.exports = { { files: ['**/src/interpreter/**/*.ts'], rules: { - // HF-131: an engine-thrown cell error must always state its cause. + // An engine-thrown cell error must always state its cause. // The public `CellError` constructor keeps `message` optional for custom // functions (docs/guide/custom-functions.md); this rule enforces the // stricter contract on our own code only. 'no-restricted-syntax': ['error', { selector: "NewExpression[callee.name='CellError'][arguments.length<2]", - message: 'HF-131: pass an ErrorMessage constant as the second argument — no cell error may be thrown without a cause.', + message: 'Pass an ErrorMessage constant as the second argument — no cell error may be thrown without a cause.', }, { selector: "NewExpression[callee.name='CellError'] > Identifier.arguments:nth-child(2)[name='undefined']", - message: 'HF-131: an explicit undefined message is not allowed — pass an ErrorMessage constant.', + message: 'An explicit undefined message is not allowed — pass an ErrorMessage constant.', }, ], }, From 7673ddfb883a3555b9c310469d4ffb96de5cc03f Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 23 Oct 2026 07:13:42 +0000 Subject: [PATCH 6/6] HF-131: Describe the changelog entry by what the change covers at this point Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85a86013ce..376ecfa7bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Changed -- Changed the cell errors thrown by the formula engine to always carry a message describing their cause. [#1547](https://github.com/handsontable/hyperformula/issues/1547) +- Changed the cell errors produced while evaluating a formula to carry a message describing their cause. [#1547](https://github.com/handsontable/hyperformula/issues/1547) ### Fixed