zed-industries/zed · error
native font ID space exhausted
Error message
native font ID space exhausted
What it means
When resolving a Font descriptor, the web text system delegates to the native font provider and then tags the resulting FontId with CANVAS_FONT_BIT only for Canvas fallback fonts. To keep the two ID spaces disjoint, it asserts the native provider never returns an ID with the CANVAS_FONT_BIT already set. If it does, the native font ID space is considered exhausted/corrupted and this error is thrown.
Solutions
- Confirm the FontId passed to font_id originated from the native provider, not from a Canvas fallback font (which has CANVAS_FONT_BIT set)
- Check font caching so Canvas-tagged IDs are never routed back through native.font_id()
- Inspect the native font provider for ID reuse/wraparound bugs; log the offending font_id.0 value to confirm bit corruption
- If seen on a 32-bit or custom target, review the CANVAS_FONT_BIT split which leaves only half the space for native IDs
Example fix
// before
let font_id = self.native.font_id(font)?;
ensure!(
font_id.0 & CANVAS_FONT_BIT == 0,
"native font ID space exhausted"
);
// after
let font_id = self.native.font_id(font)?;
ensure!(
font_id.0 & CANVAS_FONT_BIT == 0,
"native font ID space exhausted (id={:x})",
font_id.0
); // offending id indicates a stale Canvas-tagged FontId was passed in Defensive patterns
Strategy: type-guard
Validate before calling
fn safe_font_id(id: FontId) -> Option<FontId> {
(id.0 & CANVAS_FONT_BIT == 0).then_some(id)
}
// only pass safe_font_id results back into native font resolution Type guard
fn is_native_font_id(id: FontId) -> bool {
id.0 & CANVAS_FONT_BIT == 0
} Try / catch
match text_system.font_id(&font) {
Ok(id) => id,
Err(err) if err.to_string().contains("native font ID space") => {
// the FontId routed here was Canvas-tagged or corrupted; re-resolve from the descriptor
re_resolve_font(&font)?
}
Err(err) => return Err(err),
} Prevention
- Keep FontId provenance explicit: never feed a Canvas fallback FontId back into font_id()
- Store Font objects (descriptors), not raw FontIds, in long-lived app state
- Treat FontId as opaque; never serialize or persist it
- If implementing a custom native provider, ensure issued IDs never set the top bit
When it happens
Trigger: Calling font_id(&Font) when self.native.font_id(font) returns a FontId whose high bit (CANVAS_FONT_BIT, 1 << (usize::BITS - 1)) is set — i.e. the native provider has handed out an ID colliding with the Canvas half of the space, or a stale/corrupted FontId was fed back through the resolution path.
Common situations: A FontId from a Canvas fallback font being passed back into font_id() as if it were a native font (mixing up ID origins after caching), or a native backend bug/wraparound after an enormous number of font loads. Ordinary font-family resolution will not hit this.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- Canvas font ID space exhausted
- Canvas readback option could not be set
- Canvas returned a negative advance
- Canvas returned non-finite text metrics
- Canvas text bounds exceed device coordinates
AI-assisted analysis of zed-industries/zed@916fc2b8cb (2026-09-19).
Data as JSON: /api/errors/8504b5d667ce1987.
Report an issue: GitHub.
Appendix: source
Thrown at crates/gpui_web/src/text_system.rs:267
impl PlatformTextSystem for WebTextSystem {
fn set_missing_glyph_sink(&self, sink: Option<Arc<dyn MissingGlyphSink>>) {
// Cosmic must not report missing glyphs before Canvas has a chance to replace them.
self.state.write().missing_glyph_sink = sink;
}
fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
self.native.add_fonts(fonts)?;
self.state.write().measurements.clear();
Ok(())
}
fn all_font_names(&self) -> Vec<String> {
self.native.all_font_names()
}
fn font_id(&self, font: &Font) -> Result<FontId> {
let font_id = self.native.font_id(font)?;
ensure!(
font_id.0 & CANVAS_FONT_BIT == 0,
"native font ID space exhausted"
);
let mut descriptor = font.clone();
(descriptor.weight, descriptor.style) = self.native.font_weight_and_style(font_id)?;
self.state
.write()
.descriptors
.entry(font_id)
.or_insert(descriptor);
Ok(font_id)
}
fn prewarm_fonts(&self, font_ids: &[FontId]) {
if font_ids
.iter()
.all(|font_id| font_id.0 & CANVAS_FONT_BIT == 0)
{View on GitHub (pinned to 916fc2b8cb)