Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2098 (2023-07-04 13:30:12)

feat: Tasks code block now shows any error messages from searches
<!--- Provide a general summary of your changes in the Title above -->

# Description

Now if `filter by function` generates an error during searches, the error message is display in the Tasks code block, in Reading and Live Preview views.

A later change will include the instruction line in the error message.

## Motivation and Context

This is necessary for users to detect any errors in their custom filter instructions.

There is also a small chance that this may solve some of the reported issues around task searches being stuck at 'Loading tasks'.

## How has this been tested?

- By adding new tests
- By exploratory testing in the demo vault

## Screenshots (if appropriate)

![image](https://github.com/obsidian-tasks-group/obsidian-tasks/assets/4840096/8742d485-edc6-40f4-b3e6-37883686c1a9)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **New feature** (prefix: `feat` - non-breaking change which adds functionality)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Query/Filter/FunctionField.ts b/src/Query/Filter/FunctionField.ts
index 970a934b..73b55d88 100644
--- a/src/Query/Filter/FunctionField.ts
+++ b/src/Query/Filter/FunctionField.ts
@@ -85,7 +85,7 @@ function createFilterFunctionFromLine(expression: TaskExpression): FilterFunctio
 
 export function filterByFunction(expression: TaskExpression, task: Task): boolean {
     // Allow exceptions to propagate to caller, since this will be called in a tight loop.
-    // In searches, it will eventually be caught by Query.applyQueryToTasks().
+    // In searches, it will be caught by Query.applyQueryToTasks().
     const result = expression.evaluate(task);
 
     // We insist that 'filter by function' returns booleans,
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index dcbb48c0..5550a193 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -2,6 +2,7 @@ import { LayoutOptions } from '../TaskLayout';
 import type { Task } from '../Task';
 import type { IQuery } from '../IQuery';
 import { getSettings } from '../Config/Settings';
+import { errorMessageForException } from '../lib/ExceptionTools';
 import { Sort } from './Sort';
 import type { Sorter } from './Sorter';
 import { TaskGroups } from './TaskGroups';
@@ -172,21 +173,26 @@ export class Query implements IQuery {
     }
 
     public applyQueryToTasks(tasks: Task[]): QueryResult {
-        this.filters.forEach((filter) => {
-            tasks = tasks.filter(filter.filterFunction);
-        });
+        try {
+            this.filters.forEach((filter) => {
+                tasks = tasks.filter(filter.filterFunction);
+            });
 
-        const { debugSettings } = getSettings();
-        const tasksSorted = debugSettings.ignoreSortInstructions ? tasks : Sort.by(this.sorting, tasks);
-        const tasksSortedLimited = tasksSorted.slice(0, this.limit);
+            const { debugSettings } = getSettings();
+            const tasksSorted = debugSettings.ignoreSortInstructions ? tasks : Sort.by(this.sorting, tasks);
+            const tasksSortedLimited = tasksSorted.slice(0, this.limit);
 
-        const taskGroups = new TaskGroups(this.grouping, tasksSortedLimited);
+            const taskGroups = new TaskGroups(this.grouping, tasksSortedLimited);
 
-        if (this._taskGroupLimit !== undefined) {
-            taskGroups.applyTaskLimit(this._taskGroupLimit);
-        }
+            if (this._taskGroupLimit !== undefined) {
+                taskGroups.applyTaskLimit(this._taskGroupLimit);
+            }
 
-        return new QueryResult(taskGroups);
+            return new QueryResult(taskGroups);
+        } catch (e) {
+            const description = 'Search failed';
+            return QueryResult.fromError(errorMessageForException(description, e));
+        }
     }
 
     private parseHideOptions({ line }: { line: string }): void {
diff --git a/src/Query/QueryResult.ts b/src/Query/QueryResult.ts
index 75e53008..acf8746a 100644
--- a/src/Query/QueryResult.ts
+++ b/src/Query/QueryResult.ts
@@ -1,13 +1,22 @@
-import type { TaskGroups } from './TaskGroups';
+import { TaskGroups } from './TaskGroups';
 import type { TaskGroup } from './TaskGroup';
 
 export class QueryResult {
     public readonly taskGroups: TaskGroups;
+    private _searchErrorMessage: string | undefined = undefined;
 
     constructor(groups: TaskGroups) {
         this.taskGroups = groups;
     }
 
+    public get searchErrorMessage(): string | undefined {
+        return this._searchErrorMessage;
+    }
+
+    private set searchErrorMessage(value: string | undefined) {
+        this._searchErrorMessage = value;
+    }
+
     public get totalTasksCount(): number {
         return this.taskGroups.totalTasksCount();
     }
@@ -15,4 +24,10 @@ export class QueryResult {
     public get groups(): TaskGroup[] {
         return this.taskGroups.groups;
     }
+
+    static fromError(message: string): QueryResult {
+        const result = new QueryResult(new TaskGroups([], []));
+        result._searchErrorMessage = message;
+        return result;
+    }
 }
diff --git a/src/QueryRenderer.ts b/src/QueryRenderer.ts
index 186931f5..34b7de3f 100644
--- a/src/QueryRenderer.ts
+++ b/src/QueryRenderer.ts
@@ -169,6 +169,12 @@ class QueryRenderChild extends MarkdownRenderChild {
         }
 
         const queryResult = this.query.applyQueryToTasks(tasks);
+        if (queryResult.searchErrorMessage !== undefined) {
+            // There was an error in the search, for example due to a problem custom function.
+            this.renderErrorMessage(content, queryResult.searchErrorMessage);
+            return;
+        }
+
         await this.addAllTaskGroups(queryResult.taskGroups, content);
 
         const totalTasksCount = queryResult.totalTasksCount;


Test patch:
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index ee1ee269..4213c06c 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -1024,4 +1024,21 @@ At most 8 tasks per group (if any "group by" options are supplied).
             `);
         });
     });
+
+    describe('error handling', () => {
+        it('should catch an exception that occurs during searching', () => {
+            // Arrange
+            const input = 'filter by function wibble';
+            const query = new Query({ source: input });
+            const task = TaskBuilder.createFullyPopulatedTask();
+
+            // Act
+            const queryResult = query.applyQueryToTasks([task]);
+
+            // Assert
+            expect(queryResult.searchErrorMessage).toEqual(
+                'Error: Search failed. The error message was: "ReferenceError: wibble is not defined"',
+            );
+        });
+    });
 });
diff --git a/tests/Query/QueryResult.test.ts b/tests/Query/QueryResult.test.ts
index e0190614..3e924121 100644
--- a/tests/Query/QueryResult.test.ts
+++ b/tests/Query/QueryResult.test.ts
@@ -16,5 +16,16 @@ describe('QueryResult', () => {
         // Assert
         expect(queryResult.totalTasksCount).toEqual(0);
         expect(queryResult.groups).toEqual(groups.groups);
+        expect(queryResult.searchErrorMessage).toBeUndefined();
+    });
+
+    it('should be able to store an error message if the search fails', () => {
+        // Arrange, Act:
+        const message = 'I did not work';
+        const result = QueryResult.fromError(message);
+
+        // Assert
+        expect(result.searchErrorMessage).toEqual(message);
+        expect(result.taskGroups.totalTasksCount()).toEqual(0);
     });
 });