Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error handling #5407

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/silver-experts-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'slate': minor
'slate-react': minor
---

Breaking changes:

- Enhanced error handling by introducing **`undefined`** checks
- Added **`editor.onError`** function to handle errors in functions that depend on **`editor`**
- Introduced global **`ErrorLogger`** interface for error handling in functions not depending on **`editor`**
- No errors are thrown by default, addressing community feedback from issue #3641
- Updated return types of several functions to include **`| undefined`**
- Replaced **`throw new Error()`** statements with either **`editor.onError()`** or **`ErrorLogger.onError()`**
- Implemented conditional checks for variables before accessing them to prevent crashes and improve code stability
3 changes: 2 additions & 1 deletion docs/Summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
- [Rendering](concepts/09-rendering.md)
- [Serializing](concepts/10-serializing.md)
- [Normalizing](concepts/11-normalizing.md)
- [TypeScript](concepts/12-typescript.md)
- [Error Handling](concepts/12-error-handling.md)
- [TypeScript](concepts/13-typescript.md)
zbeyens marked this conversation as resolved.
Show resolved Hide resolved
- [Migrating](concepts/xx-migrating.md)

## API
Expand Down
10 changes: 5 additions & 5 deletions docs/api/locations/path.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,19 @@ The paths are sorted from shallowest to deepest. However, if the `reverse: true`

Options: `{reverse?: boolean}`

#### `Path.next(path: Path) => Path`
#### `Path.next(path: Path) => Path | undefined`

Given a path, gets the path to the next sibling node. The method does not ensure that the returned `Path` is valid in the document.

#### `Path.parent(path: Path) => Path`
#### `Path.parent(path: Path) => Path | undefined`

Given a path, return a new path referring to the parent node above it. If the `path` argument is equal to `[]`, throws an error.

#### `Path.previous(path: Path) => Path`
#### `Path.previous(path: Path) => Path | undefined`

Given a path, get the path to the previous sibling node. The method will throw an error if there are no previous siblings (e.g. if the Path is currently `[1, 0]`, the previous path would be `[1, -1]` which is illegal and will throw an error).
Given a path, get the path to the previous sibling node. The method will return `undefined` if there are no previous siblings (e.g. if the Path is currently `[1, 0]`, the previous path would be `[1, -1]` which is illegal and will return `undefined`).

#### `Path.relative(path: Path, ancestor: Path) => Path`
#### `Path.relative(path: Path, ancestor: Path) => Path | undefined`

Given two paths, one that is an ancestor to the other, returns the relative path from the `ancestor` argument to the `path` argument. If the `ancestor` path is not actually an ancestor or equal to the `path` argument, throws an error.

Expand Down
22 changes: 11 additions & 11 deletions docs/api/nodes/editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,27 +87,27 @@ If there is no point before the location (e.g. we are at the top of the document

Options: `{distance?: number, unit?: 'offset' | 'character' | 'word' | 'line' | 'block', voids?: boolean}`

#### `Editor.edges(editor: Editor, at: Location) => [Point, Point]`
#### `Editor.edges(editor: Editor, at: Location) => [Point | undefined, Point | undefined]`

Get the start and end points of a location.

#### `Editor.end(editor: Editor, at: Location) => Point`
#### `Editor.end(editor: Editor, at: Location) => Point | undefined`

Get the end point of a location.

#### `Editor.first(editor: Editor, at: Location) => NodeEntry`
#### `Editor.first(editor: Editor, at: Location) => NodeEntry | undefined`

Get the first node at a location.

#### `Editor.fragment(editor: Editor, at: Location) => Descendant[]`

Get the fragment at a location.

#### `Editor.last(editor: Editor, at: Location) => NodeEntry`
#### `Editor.last(editor: Editor, at: Location) => NodeEntry | undefined`

Get the last node at a location.

#### `Editor.leaf(editor: Editor, at: Location, options?) => NodeEntry`
#### `Editor.leaf(editor: Editor, at: Location, options?) => NodeEntry | undefined`

Get the leaf text node at a location.

Expand All @@ -131,7 +131,7 @@ Note: To find the next Point, and not the next Node, use the `Editor.after` meth

Options: `{at?: Location, match?: NodeMatch, mode?: 'all' | 'highest' | 'lowest', voids?: boolean}`

#### `Editor.node(editor: Editor, at: Location, options?) => NodeEntry`
#### `Editor.node(editor: Editor, at: Location, options?) => NodeEntry | undefined`

Get the node at a location.

Expand All @@ -151,19 +151,19 @@ Options: `{at?: Location | Span, match?: NodeMatch, mode?: 'all' | 'highest' | '
- `'highest'`: in a hierarchy of nodes, only return the highest level matching nodes
- `'lowest'`: in a hierarchy of nodes, only return the lowest level matching nodes

#### `Editor.parent(editor: Editor, at: Location, options?) => NodeEntry<Ancestor>`
#### `Editor.parent(editor: Editor, at: Location, options?) => NodeEntry<Ancestor> | undefined`

Get the parent node of a location.

Options: `{depth?: number, edge?: 'start' | 'end'}`

#### `Editor.path(editor: Editor, at: Location, options?) => Path`
#### `Editor.path(editor: Editor, at: Location, options?) => Path | undefined`

Get the path of a location.

Options: `{depth?: number, edge?: 'start' | 'end'}`

#### `Editor.point(editor: Editor, at: Location, options?) => Point`
#### `Editor.point(editor: Editor, at: Location, options?) => Point | undefined`

Get the start or end point of a location.

Expand Down Expand Up @@ -196,11 +196,11 @@ Note: To find the previous Point, and not the previous Node, use the `Editor.bef

Options: `{at?: Location, match?: NodeMatch, mode?: 'all' | 'highest' | 'lowest', voids?: boolean}`

#### `Editor.range(editor: Editor, at: Location, to?: Location) => Range`
#### `Editor.range(editor: Editor, at: Location, to?: Location) => Range | undefined`

Get a range of a location.

#### `Editor.start(editor: Editor, at: Location) => Point`
#### `Editor.start(editor: Editor, at: Location) => Point | undefined`

Get the start point of a location.

Expand Down
24 changes: 12 additions & 12 deletions docs/api/nodes/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,33 @@

### Retrieval methods

#### `Node.ancestor(root: Node, path: Path) => Ancestor`
#### `Node.ancestor(root: Node, path: Path) => Ancestor | undefined`

Get the node at a specific `path`, asserting that it is an ancestor node. If the specified node is not an ancestor node, throw an error.
Get the node at a specific `path`, asserting that it is an ancestor node. If the specified node is not an ancestor node, return `undefined`.

#### `Node.ancestors(root: Node, path: Path, options?) => Generator<NodeEntry<Ancestor>>`

Return a generator of all the ancestor nodes above a specific path. By default, the order is top-down, from highest to lowest ancestor in the tree, but you can pass the `reverse: true` option to go bottom-up.

Options: `{reverse?: boolean}`

#### `Node.child(root: Node, index: number) => Descendant`
#### `Node.child(root: Node, index: number) => Descendant | undefined`

Get the child of a node at the specified `index`.

#### `Node.children(root: Node, path: Path, options?) => Generator<NodeEntry<Descendant>>`
#### `Node.children(root: Node, path: Path, options?) => Generator<NodeEntry<Descendant>> | undefined`

Iterate over the children of a node at a specific path.

Options: `{reverse?: boolean}`

#### `Node.common(root: Node, path: Path, another: Path) => NodeEntry`
#### `Node.common(root: Node, path: Path, another: Path) => NodeEntry | undefined`

Get an entry for the common ancestor node of two paths. It might be a Text node, an Element, or the Editor itself.

For the common block ancestor, see [Editor Selection](https://docs.slatejs.org/concepts/03-locations#selection)

#### `Node.descendant(root: Node, path: Path) => Descendant`
#### `Node.descendant(root: Node, path: Path) => Descendant | undefined`

Get the node at a specific path, asserting that it's a descendant node.

Expand All @@ -51,25 +51,25 @@ Return a generator of all the element nodes inside a root node. Each iteration w

Options: `{from?: Path, to?: Path, reverse?: boolean, pass?: (node: NodeEntry => boolean)}`

#### `Node.first(root: Node, path: Path) => NodeEntry`
#### `Node.first(root: Node, path: Path) => NodeEntry | undefined`

Get the first node entry in a root node from a `path`.

#### `Node.fragment(root: Node, range: Range) => Descendant[]`

Get the sliced fragment represented by the `range`.

#### `Node.get(root: Node, path: Path) => Node`
#### `Node.get(root: Node, path: Path) => Node | undefined`

Get the descendant node referred to by a specific `path`. If the path is an empty array, get the root node itself.

#### `Node.last(root: Node, path: Path) => NodeEntry`
#### `Node.last(root: Node, path: Path) => NodeEntry | undefined`

Get the last node entry in a root node at a specific `path`.

#### `Node.leaf(root: Node, path: Path) => Text`
#### `Node.leaf(root: Node, path: Path) => Text | undefined`

Get the node at a specific `path`, ensuring it's a leaf text node. If the node is not a leaf text node, throw an error.
Get the node at a specific `path`, ensuring it's a leaf text node. If the node is not a leaf text node, return `undefined`.

#### `Node.levels(root: Node, path: Path, options?) => Generator<NodeEntry>`

Expand All @@ -83,7 +83,7 @@ Return a generator of all the node entries of a root node. Each entry is returne

Options: `{from?: Path, to?: Path, reverse?: boolean, pass?: (node: NodeEntry => boolean)}`

#### `Node.parent(root: Node, path: Path) => Ancestor`
#### `Node.parent(root: Node, path: Path) => Ancestor | undefined`

Get the parent of a node at a specific `path`.

Expand Down
59 changes: 59 additions & 0 deletions docs/concepts/12-error-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Error Handling

Error handling in Slate is designed to provide more control to users while maintaining code stability, maintainability, and robustness. Slate introduces a global **`ErrorLogger`** interface and an **`editor.onError`** function to handle errors in a more flexible and manageable way.

Remember to utilize these error handling mechanisms to tailor your error management based on your specific use case and requirements, ensuring a smoother and more stable experience for your users. For example, you may want to throw errors in development, but in production you would prefer to have non-blocking errors for your users.

## `ErrorLogger`

The global **`ErrorLogger`** interface is used to handle errors in functions not depending on the **`editor`**, like `Node.get`. To set custom error handling behavior, call **`ErrorLogger.setOnError()`**.

To throw errors on invalid operations:

```tsx
import { ErrorLogger } from 'slate'

ErrorLogger.setOnError(error => {
throw new Error(error.message)
})
```

You can also filter errors by type:

```tsx
ErrorLogger.setOnError(error => {
if (error.type === 'Node.get') {
throw new Error(error.message)
}
// ...push the error to ErrorLogger.errors
})
```

## `editor.onError`

The **`editor.onError`** function is used to handle errors in functions that depend on the **`editor`**.

To set custom error handling behavior for the editor, override the **`editor.onError`** function.

To throw errors on invalid operations:

```tsx
editor.onError = error => {
throw new Error(error.message)
}
```

You can also filter errors by type:

```tsx
// throw only for `move_node` operation
editor.onError = error => {
if (error.type === 'move_node') {
throw new Error(error.message)
}
}
```

## `editor.errors`

By default, Slate will push errors to `editor.errors` so you can make sure this array stays empty.
File renamed without changes.
16 changes: 8 additions & 8 deletions docs/libraries/slate-react/react-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,39 +50,39 @@ Find a key for a Slate node.

Returns an instance of `Key` which looks like `{ id: string }`

#### `ReactEditor.findPath(editor: ReactEditor, node: Node): Path`
#### `ReactEditor.findPath(editor: ReactEditor, node: Node): Path | undefined`

Find the path of Slate node.

#### `ReactEditor.hasDOMNode(editor: ReactEditor, target: DOMNode, options: { editable?: boolean } = {}): boolean`

Check if a DOM node is within the editor.

#### `ReactEditor.toDOMNode(editor: ReactEditor, node: Node): HTMLElement`
#### `ReactEditor.toDOMNode(editor: ReactEditor, node: Node): HTMLElement | undefined`

Find the native DOM element from a Slate node.

#### `ReactEditor.toDOMPoint(editor: ReactEditor, point: Point): DOMPoint`
#### `ReactEditor.toDOMPoint(editor: ReactEditor, point: Point): DOMPoint | undefined`

Find a native DOM selection point from a Slate point.

#### `ReactEditor.toDOMRange(editor: ReactEditor, range: Range): DOMRange`
#### `ReactEditor.toDOMRange(editor: ReactEditor, range: Range): DOMRange | undefined`

Find a native DOM range from a Slate `range`.

#### `ReactEditor.toSlateNode(editor: ReactEditor, domNode: DOMNode): Node`
#### `ReactEditor.toSlateNode(editor: ReactEditor, domNode: DOMNode): Node | undefined`

Find a Slate node from a native DOM `element`.

#### `ReactEditor.findEventRange(editor: ReactEditor, event: any): Range`
#### `ReactEditor.findEventRange(editor: ReactEditor, event: any): Range | undefined`

Get the target range from a DOM `event`.

#### `ReactEditor.toSlatePoint(editor: ReactEditor, domPoint: DOMPoint): Point | null`
#### `ReactEditor.toSlatePoint(editor: ReactEditor, domPoint: DOMPoint): Point | null | undefined`

Find a Slate point from a DOM selection's `domNode` and `domOffset`.

#### `ReactEditor.toSlateRange(editor: ReactEditor, domRange: DOMRange | DOMStaticRange | DOMSelection, options?: { exactMatch?: boolean } = {}): Range | null`
#### `ReactEditor.toSlateRange(editor: ReactEditor, domRange: DOMRange | DOMStaticRange | DOMSelection, options?: { exactMatch?: boolean } = {}): Range | null | undefined`

Find a Slate range from a DOM range or selection.

Expand Down
2 changes: 1 addition & 1 deletion docs/walkthroughs/01-installing-slate.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const App = () => {

Of course we haven't rendered anything, so you won't see any changes.

> If you are using TypeScript, you will also need to extend the `Editor` with `ReactEditor` and add annotations as per the documentation on [TypeScript](../concepts/12-typescript.md). The example below also includes the custom types required for the rest of this example.
> If you are using TypeScript, you will also need to extend the `Editor` with `ReactEditor` and add annotations as per the documentation on [TypeScript](../concepts/13-typescript.md). The example below also includes the custom types required for the rest of this example.

```typescript
// TypeScript users only add this code
Expand Down
8 changes: 4 additions & 4 deletions packages/slate-hyperscript/src/creators.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Element, Descendant, Node, Range, Text, Editor } from 'slate'
import { Descendant, Editor, Element, Node, Range, Text } from 'slate'
import {
AnchorToken,
FocusToken,
Token,
addAnchorToken,
addFocusToken,
AnchorToken,
FocusToken,
getAnchorOffset,
getFocusOffset,
Token,
} from './tokens'

/**
Expand Down
Loading
Loading