Error handling
Sometimes an editor hits a problem it cannot carry on from. CKEditor 5 tells you when that happens and leaves the decision about what to do next to your application. This guide covers how to hear about such errors and what your options are afterward.
Nothing is restarted for you, and no editor content is saved or handed back. For why it works this way, see the What we deliberately do not do section.
Register a callback with onEditorError(). It returns a function that unregisters the callback:
import { onEditorError } from 'ckeditor5';
const off = onEditorError( ( { error, source } ) => {
console.error( 'An error escaped', source, error );
} );Copy codeKeep the returned function if you will ever want to stop listening, and call off() at that point.
There is one registration surface for the whole page rather than one per editor. You can register as many callbacks as you like, and each unregisters on its own.
The same function is also available as a static field on every editor and context class:
const off = ClassicEditor.onEditorError( ( { error, source } ) => {
// ...
} );Copy codeThe static field matters when your code is handed an editor class instead of importing one, which is how the framework integrations work: importing anything from CKEditor 5 as a value would load the npm build and stop the editor from being loaded from a CDN, so they reach for the class you passed to them. See the Framework integrations section.
The callback gets one object with two properties.
-
erroris theCKEditorErrorthat escaped. -
sourceis the editor or theContextthat the error was attributed to. You can use the property to compare it with your own instance to tell whether the error came from an editor you manage:
onEditorError( ( { error, source } ) => {
if ( source !== myEditor ) {
return;
}
reportToMyErrorTracker( error );
} );Copy codeWhen several editors on one page share an object, an error can be attributed to more than one of them. That happens when you pass the same configuration object or the same plugins array to each editor, and with features that keep state shared between editors, such as the list feature. The callback is then called once for each of these editors, with the same error and a different source, so an editor may be reported for an error that did not come from it. Give every editor a configuration of its own to avoid the first case.
In TypeScript source is typed Editor | Context | null. It is never null today, because an error that cannot be attributed is not reported at all, but the type leaves room for that to change.
The object that actually threw is in error.context. It is whatever the throwing code had at hand, such as a plugin, a command, the model, or a writer, so it is rarely what you want to act on.
Only a CKEditorError reaches your callback, and only while the editor it belongs to is ready. An error attributed to a Context has no such condition. An error that cannot be attributed at all is not reported.
That covers more than it may sound like. The editor wraps unexpected errors itself, through CKEditorError.rethrowUnexpectedError(), in model.change() and enqueueChange(), in view rendering, in editor.execute(), and in the emitter mixins. Anything thrown from a listener on a CKEditor 5 emitter, therefore, arrives already wrapped, together with the object it came from.
Reporting picks errors up from the page rather than from the editor: it listens for the error and unhandledrejection events on window. So an error does not have to be thrown inside a call you made on the editor to be reported. One thrown asynchronously from inside a plugin, out of a timeout or a rejected promise, reaches your callback just the same, as long as it is a CKEditorError that can be attributed.
An ordinary Error thrown by your own code, outside any editor’s emitter or change block, is not an editor error and is not reported. Neither is an error you catch yourself, wherever it came from: nothing reaches the page, so nothing is picked up.
Nothing is swallowed. Every reported error still reaches the console, exactly as it does without a handler registered.
The editor stays as it is, and nothing stops the user from editing further. That does not mean its state is consistent, though. An error thrown in the middle of an operation, for example while the user is typing, can leave what is on screen out of sync with the editor data, the text typed afterward may never reach it, and undo may stop working. So do not leave a reported error unhandled.
Whether the state is worth keeping is a judgment only your application can make, which is why the decision is yours. You have three options, and they combine.
Switching the editor to read-only stops further adjustments without throwing anything away, which gives you room to decide:
onEditorError( ( { source } ) => {
if ( source === myEditor ) {
myEditor.enableReadOnlyMode( 'error-handling' );
}
} );Copy codeThe lock is released with disableReadOnlyMode( 'error-handling' ) when you are ready to let editing continue. Features may also switch the editor to read-only on their own for errors they consider critical. See the read-only mode guide for how the locks work.
Destroy the instance and create a new one. Declare the reference with let, so that you can point it at the new editor afterward:
let myEditor = await ClassicEditor.create( { /* ... */ } );
onEditorError( ( { source } ) => {
if ( source !== myEditor ) {
return;
}
recreate().catch( error => console.error( error ) );
} );
async function recreate() {
await myEditor.destroy();
myEditor = await ClassicEditor.create( { /* ... */ } );
}Copy codeThe callback does not need re-registering. It reads myEditor every time it runs, so reassigning the variable is enough for the comparison to match the new editor. Copying the instance into another variable at registration time is what would break it, because that copy keeps pointing at the destroyed editor.
The new editor’s content comes from whatever you pass in, through config.root.initialData. CKEditor 5 does not keep a copy for you, so it has to come from your own source – see the Recover the content section for where to look.
Do not recreate on every error. An error that keeps happening will make the editor fail, be recreated, and fail again for as long as the page is open, so decide how many attempts you are willing to make.
In a framework integration you do not write the destroying and creating yourself, because remounting the component does it, and the component reattaches its own error callback too. Giving the remounted component its content back is still yours to do. See the migrating from the Watchdog guide for how to remount in React, Vue, and Angular.
You already hold a better copy of the content than the editor could hand you. There are a few places to look, depending on your setup.
- Your own backend. If you used the autosave feature, your application has been saving the content at points where it was known to be complete.
- Cloud Services document storage. If it is enabled for your environment,
GET /storage/{document_id}returns the stored content of a document. See the document storage guide for what is stored and how to read it back. - The editor itself, as a last resort. The editor is still running, so you can try
editor.getData()from the handler, but it may also throw. Even when it answers, it is the one source with no guarantee behind it: the error may have interrupted an operation halfway, which is why CKEditor 5 does not hand you this data by itself. If you have no other source, prefer it to losing the content, and treat what you get as needing review rather than as a clean save.
The React, Vue, and Angular components report errors through their own callback, so you usually do not register onEditorError() yourself.
Errors attributed to a Context rather than to a single editor are a separate case. In React the <CKEditorContext> component reports them through its own onError. Vue and Angular have no context component, so register a callback yourself off the context class.
Skip this section unless you upload an editor bundle to Cloud Services, so that it can run an editor on the server without a browser.
Error handling is a browser-side mechanism, and it does not apply there. The bundle exports an editor class, and Cloud Services creates the instance itself, so there is nowhere for you to register a callback. Cloud Services reports errors from server-side scripts on its own instead.
- Nothing restarts. Not automatically, and not as an option you can switch on.
- No content is saved or handed back. An error can happen while an operation is only half applied, so a copy taken at that moment may be a state that never legitimately existed. Rather than hand you data we cannot be sure about, we leave recovery to the sources listed above, most of which are written at points where the content is known to be complete.
- No crash history. The callback reports each error as it happens, and keeping a record of them is up to your application.
- No per-editor or configuration-based registration. There is one page-level function, and you compare
sourcewith your own instance.
Errors thrown while an editor is being created or destroyed are not reported. Reporting follows a running editor: one counts as running from the moment it becomes ready until it starts being destroyed, so an error from outside that window has no editor to be attributed to. There would also be little you could do with one that is half-built or already gone.
Those errors are not lost, though. Both create() and destroy() return a promise, so catch them there:
ClassicEditor
.create( { /* ... */ } )
.catch( error => {
console.error( error );
} );Copy codeAn error at either stage usually means the editor is being integrated incorrectly rather than that something went wrong at runtime. See the editor lifecycle guide for both methods.