This guide puts every feature together for a Fabric.js editor in production: storage, autosave, recovery, versions, limits and assets, plus error handling for each failure.
The engine
import {
bindKeyboardShortcuts,
bindUnsavedChangesWarning,
createDocumentEngine,
} from "fabricjs-document-engine";
import { createIndexedDbRecovery } from "fabricjs-document-engine/recovery";
export function createEditor(canvas: Canvas) {
const engine = createDocumentEngine({
canvas,
storage: restStorage,
customObjects: [{ fabricClass: Sticker, properties: ["label"] }],
autosave: { delay: 1500, maxWait: 15000 },
saveRetry: { attempts: 4, baseDelay: 800, maxDelay: 10000 },
recovery: { store: createIndexedDbRecovery(), interval: 3000 },
versions: { autoEvery: 20, keepAuto: 30 },
history: { limit: 150, maxBytes: 48 * 1024 * 1024 },
limits: {
maxObjects: 20000,
isAllowedUrl: (url) =>
url.startsWith("https://cdn.example.com/") || url.startsWith("blob:"),
},
assets: {
resolveUrl: (url) => signCdnUrl(url),
upload: async ({ blob }) => uploadToBucket(blob),
replaceMissingImage: () =>
"https://cdn.example.com/placeholders/missing.png",
},
});
const unbindShortcuts = bindKeyboardShortcuts(engine);
const unbindWarning = bindUnsavedChangesWarning(engine);
return {
engine,
dispose() {
unbindShortcuts();
unbindWarning();
engine.destroy();
},
};
}Opening a document
Check for an interrupted load and for recovery copies before opening:
async function openDocument(engine: DocumentEngine, id: string) {
const interrupted = await engine.getInterruptedLoad();
if (
interrupted?.documentId === id &&
!(await confirmUser("This document failed to open last time. Try again?"))
) {
return;
}
const copy = await engine.getRecovery(id);
if (copy && (await confirmUser("Restore unsaved work?"))) {
await engine.restoreRecovery(id, { discardUnsavedChanges: true });
return;
}
if (copy) await engine.discardRecovery(id);
await engine.load(id);
}Error handling
Every failure is a DocumentEngineError with a stable code. Switch on the code, never on the message:
engine.on("load:error", ({ error }) => {
switch (error.code) {
case "LOAD_ABORTED":
return;
case "MISSING_ASSETS":
return showMissingImages(error.missingAssets);
case "UNKNOWN_OBJECT_TYPE":
return showMessage("This document needs a newer version of the editor.");
case "UNSUPPORTED_SCHEMA":
return showMessage(
"This document was made with a newer version of the editor."
);
case "UNSAFE_DOCUMENT":
return showMessage("This file was blocked for safety reasons.");
default:
return showMessage(`The document could not be opened: ${error.message}`);
}
});
engine.on("save:error", ({ error }) => {
if (error.code === "SAVE_CONFLICT") return askConflict();
if (error.code !== "SAVE_CANCELLED") {
showMessage("Saving failed. Your work is kept on this device.");
}
});Every code is listed in error codes.
Leaving the editor
async function closeEditor(editor: ReturnType<typeof createEditor>) {
if (editor.engine.isDirty())
await editor.engine.save().catch(() => undefined);
editor.dispose();
}destroy() also writes a recovery copy of unsaved work, so nothing is lost when that last save fails.
On the server with Node.js
The document helpers run in Node.js without a canvas. Check every upload before you store it, as shown in security.
Checklist
Before you launch a Fabric.js editor in production, go through this checklist:
-
verifyStorageAdapter(yourStorage)passes. See verify an adapter. - The server compares the revision and writes in one query.
- Custom classes are registered everywhere documents are opened.
- Web fonts load through
assets.loadFont. - Images load with CORS and
crossOrigin: "anonymous", so exports work. - You have UI for
SAVE_CONFLICT,MISSING_ASSETS, recovery offers andUNSAVED_CHANGES. - Your toolbar follows the accessibility guide.
A Fabric.js editor in production that passes this list handles the failures users actually hit.