How to Fix: `Access::instance` panics

6 min read

Access::instance panic: why it happens and how to fix it safely

This panic is a classic invalid state bug: an Access value gets created in a mode where no backing instance exists, but Access::instance() still assumes one is always available and crashes when that assumption is false. If you hit this while using bindgen with the store option but without async, or by directly calling instance() on the result of Access::new, the failure is reproducible for the same reason: the API surface permits a call path that violates its own runtime invariants.

Understanding the Root Cause

The panic occurs because Access::instance() is being used as though every Access object is guaranteed to wrap a valid instance handle. In the failing scenarios described in the issue, that guarantee does not hold.

Technically, the problem usually looks like this:

  • Access::new can construct an Access in more than one internal mode.
  • One mode is backed by an actual instance.
  • Another mode is valid for store-based or non-async workflows, but does not carry an instance.
  • Access::instance() then performs an unconditional unwrap, match arm, or internal assumption that an instance is present.

That means the bug is not simply that the caller did something wrong. The real issue is an API contract mismatch: construction allows a state that lookup does not safely handle.

In practice, this often appears in code shaped like one of these patterns:

// conceptual example only
let access = Access::new(...);
let instance = access.instance(); // panics if Access was created without an instance

Or indirectly:

// conceptual example only
bindgen_options.store = true;
bindgen_options.async = false;
// generated or internal code eventually calls access.instance()

If the implementation stores something like Option<Instance>, the panic is commonly caused by logic equivalent to:

// conceptual example only
pub fn instance(&self) -> &Instance {
    self.instance.as_ref().unwrap()
}

That design is fragile because it encodes a runtime invariant without enforcing it at compile time.

Step-by-Step Solution

The safest fix is to make Access::instance() reflect reality: an instance is not always available. Then update call sites to handle that case explicitly.

Recommended approach: change the method to return an Option or Result instead of panicking.

1. Locate the implementation of Access

Find where Access stores its internal state and where instance() is defined. You are looking for code that assumes the presence of an instance even when the constructor can create an instance-less value.

2. Replace the panic-prone accessor

Refactor the accessor so it exposes the optional nature of the value.

// before
impl Access {
    pub fn instance(&self) -> &Instance {
        self.instance.as_ref().unwrap()
    }
}
// after: Option-based fix
impl Access {
    pub fn instance(&self) -> Option<&Instance> {
        self.instance.as_ref()
    }
}

If the codebase prefers explicit error propagation, use Result instead:

// after: Result-based fix
impl Access {
    pub fn instance(&self) -> Result<&Instance, Error> {
        self.instance
            .as_ref()
            .ok_or(Error::MissingInstanceForAccess)
    }
}

3. Update all call sites

Any code that currently assumes an unconditional instance must now branch correctly.

// before
let instance = access.instance();
use_instance(instance);
// after with Option
if let Some(instance) = access.instance() {
    use_instance(instance);
} else {
    // handle store-only or non-instance mode
    handle_missing_instance();
}
// after with Result
let instance = access.instance()?;
use_instance(instance);

4. Guard the bindgen store path

The issue description specifically mentions using store without async. That combination likely reaches a path where an instance is not initialized. Add a defensive branch around the code that reads from Access::instance().

// conceptual logic
if options.store && !options.r#async {
    // avoid requiring instance-backed access here
    // use store-specific logic instead
} else {
    let instance = access.instance().ok_or(Error::MissingInstanceForAccess)?;
    // normal instance-backed logic
}

5. Tighten the type model if possible

The best long-term fix is often to avoid one struct representing multiple incompatible states. Instead of one Access type with optional fields, split it into distinct variants.

enum Access {
    InstanceBacked { instance: Instance },
    StoreBacked { store: Store },
}

impl Access {
    pub fn instance(&self) -> Option<&Instance> {
        match self {
            Access::InstanceBacked { instance } => Some(instance),
            Access::StoreBacked { .. } => None,
        }
    }
}

This makes the valid states more explicit and reduces future regressions.

6. Add regression tests

You should add tests for both direct and indirect reproduction paths.

#[test]
fn access_instance_does_not_panic_when_instance_is_missing() {
    let access = Access::new(/* construct non-instance mode */);
    assert!(access.instance().is_none());
}

#[test]
fn bindgen_store_without_async_does_not_trigger_instance_panic() {
    let options = BindgenOptions {
        store: true,
        r#async: false,
        ..Default::default()
    };

    let result = run_bindgen(options);
    assert!(result.is_ok());
}

If the project uses panic assertions to validate old behavior, replace them with assertions on graceful error handling.

7. If API compatibility matters, introduce a non-panicking method first

If changing the signature of instance() would be too disruptive, add a new safe accessor and gradually migrate callers.

impl Access {
    pub fn try_instance(&self) -> Option<&Instance> {
        self.instance.as_ref()
    }

    pub fn instance(&self) -> &Instance {
        self.try_instance()
            .expect("Access::instance called without an instance-backed Access")
    }
}

This is a good transition strategy, but it should be considered temporary. The end goal should still be eliminating panic-driven control flow where absence is expected.

Common Edge Cases

  • Generated code still assumes an instance exists: even after fixing the core type, generated bindings or helper layers may still call instance() directly. Search for all usages, not just the obvious ones.
  • Async and non-async code paths diverge subtly: a fix validated in async mode may still fail in non-async mode because initialization order differs.
  • Store-backed access may require separate logic: if store mode was never meant to expose an instance, callers should not silently fabricate one. They need a dedicated path.
  • Hidden unwraps in helper methods: even if Access::instance() is fixed, another method may internally unwrap the same optional state and reintroduce the panic.
  • Trait implementations can mask the bug: methods like Deref, conversion helpers, or generated wrapper methods can indirectly force instance access and make stack traces harder to interpret.
  • Backward compatibility concerns: changing a public API from &Instance to Option<&Instance> may require a major version bump if the method is public and widely consumed.

FAQ

Why does this only show up with store and no async?

Because that configuration likely creates an Access state that is valid for storage-oriented behavior but does not initialize the instance-backed branch. The panic appears only when code later assumes all access modes contain an instance.

Should I fix this with unwrap_or or a dummy instance?

No. A fallback dummy instance hides the real state mismatch and can cause deeper correctness bugs. Use Option, Result, or separate enum variants so the absence of an instance is modeled explicitly.

Is keeping the panic acceptable if callers are supposed to know better?

Usually no. If Access::new can legally produce a value without an instance, then a panicking instance() is misleading API design. Panics are best reserved for truly impossible states, not for expected configuration-dependent behavior.

The durable fix is to align construction, representation, and accessor semantics. Once Access makes the presence of an instance explicit, the Access::instance panic disappears, the bindgen store path becomes predictable, and future maintenance gets much safer.

Leave a Reply

Your email address will not be published. Required fields are marked *