AykutSarac/jsoncrack.com · error

Failed to download image!

Error message

Failed to download image!

What it means

Generic toast from exportAsImage()'s catch when html-to-image (toSvg/toPng/toJpeg) throws. Covers rendering failures: the target node contains cross-origin images/canvas that taint the output (SecurityError), a font/image fails to load, the element is too large causing memory/timeouts, or getDownloadFormat returns undefined for an unknown extension.

Source

Thrown at apps/www/src/features/modals/DownloadModal/index.tsx:138

      toast.loading("Downloading...", { id: "toastDownload" });

      const imageElement = getExportElement();
      if (!imageElement) {
        toast.error("Canvas not found.");
        return;
      }
      const imageOptions = {
        quality: fileDetails.quality,
        backgroundColor: fileDetails.backgroundColor,
        skipFonts: true,
      };

      const dataURI = await getDownloadFormat(extension)(imageElement, imageOptions);

      downloadURI(dataURI, `${fileDetails.filename}.${extension}`);
      gaEvent("download_img", { label: extension });
    } catch {
      toast.error("Failed to download image!");
    } finally {
      toast.dismiss("toastDownload");
      onClose();
    }
  };

  const updateDetails = (key: keyof typeof fileDetails, value: string | number) =>
    setFileDetails({ ...fileDetails, [key]: value });

  return (
    <Modal opened={opened} onClose={onClose} title="Download Image" centered>
      <TextInput
        label="File Name"
        value={fileDetails.filename}
        onChange={e => updateDetails("filename", e.target.value)}
        mb="lg"
      />
      <SegmentedControl

View on GitHub (pinned to 3c9af69e23)

Solutions

  1. Ensure all images/fonts rendered in the canvas are same-origin or CORS-enabled.
  2. Keep skipFonts:true (already set) to reduce font-load failures.
  3. Validate the selected extension is one of SVG/PNG/JPEG before calling getDownloadFormat.
  4. Log the caught error to identify the exact html-to-image failure (currently swallowed).

Example fix

// before
const dataURI = await getDownloadFormat(extension)(imageElement, imageOptions);
// ...
} catch {
  toast.error("Failed to download image!");
}

// after — guard the format lookup and log the real cause
const renderer = getDownloadFormat(extension);
if (!renderer) { toast.error("Unknown format."); return; }
try {
  const dataURI = await renderer(imageElement, imageOptions);
  downloadURI(dataURI, `${fileDetails.filename}.${extension}`);
} catch (err) {
  console.error(err);
  toast.error("Failed to download image!");
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate the extension maps to a renderer and content is CORS-safe
const KNOWN = new Set(["svg", "png", "jpeg"]);
export function canExport(ext: string): boolean {
  return KNOWN.has(ext);
}

Type guard

// Guard getDownloadFormat's return
function hasRenderer(fn: unknown): fn is (el: HTMLElement, opts: unknown) => Promise<string> {
  return typeof fn === "function";
}

Try / catch

// Guard format lookup, log the html-to-image cause
const renderer = getDownloadFormat(extension);
if (!hasRenderer(renderer)) { toast.error("Unknown format."); return; }
try { await renderer(el, opts); } catch (err) { console.error(err); toast.error("Failed to download image!"); }

Prevention

When it happens

Trigger: A graph referencing cross-origin images without CORS; a very large canvas triggering OOM; skipFonts is true but foreignObject still hits a tainted canvas; an extension value outside the SVG/PNG/JPEG enum making getDownloadFormat return undefined (then calling undefined() throws TypeError).

Common situations: Exporting large graphs; embedded remote images without CORS headers; Safari foreignObject quirks; runtime extension value drift.

Related errors


AI-assisted analysis of AykutSarac/jsoncrack.com@3c9af69e23 (2026-08-12). Data as JSON: /api/errors/545110a12526d00c. Report an issue: GitHub.