Quick answer: Bevy games are written in Rust, so crashes arrive as panics, typically from unwrap, expect, or out-of-bounds access inside an ECS system, plus wgpu rendering faults on bad drivers. Install a panic hook that captures the message and backtrace, attach the system and GPU context, and group reports so the same unwrap failure becomes one ranked issue.
Bevy is a data-driven Rust game engine built on an entity component system and the wgpu graphics abstraction. Because it is Rust, its crashes look nothing like a managed engine's: there are no exceptions, only panics. A panic from an unwrap on a None, an expect with a message, an out-of-bounds slice index, or an arithmetic overflow in debug unwinds the thread and, in a game, usually aborts. On top of that, wgpu can surface GPU and driver faults across Vulkan, Metal, and DirectX. This post covers how Bevy panics work, how the ECS shapes them, and how to capture backtraces and context that make them fixable.
Panics, not exceptions
Rust has no exceptions, so a Bevy crash is a panic. The classic sources are unwrap and expect on an Option or Result that turned out to be None or Err, indexing a Vec or slice out of bounds, integer overflow in a debug build, and explicit panic calls. When a system panics, Bevy's scheduler is running it on a thread, and the default behavior unwinds and typically brings the app down. The message tells you the kind of panic, but on its own it lacks the path that led there.
The crucial setting is the backtrace. Rust captures one only when RUST_BACKTRACE is enabled or when you opt in programmatically, and your release builds usually run with optimizations that can inline frames. To get useful crash reports you want backtraces enabled in your shipping builds and debug symbols retained, so the panic location points at the real system and line. Without that, you have a panic message like called Option unwrap on a None value and no idea which of your dozens of systems produced it.
How the ECS shapes crashes
Bevy's ECS runs systems as functions that query components, and it parallelizes systems whose data access does not conflict. That scheduling means a panic can come from any system in the frame, and because systems run concurrently, a crash in one is isolated to that thread but still fatal to the app. The most common ECS panics are querying a component that an entity does not have via get and unwrapping the result, or assuming a resource exists. Entity despawn timing causes many of these: a system holds an entity id whose components were removed earlier in the schedule.
Because systems are small and numerous, the panic location plus the system name is gold. If your backtrace points at the function, you immediately know which query and which component access failed. Designing systems to handle the None case with a continue or an early return, rather than unwrap, prevents many of these outright, but for the ones that slip through, capturing which system and what entities or resources it touched is what turns an abstract ECS panic into a concrete fix.
wgpu and GPU driver faults
Bevy renders through wgpu, which targets Vulkan, Metal, DirectX 12, and others depending on platform. This layer is a frequent source of crashes that are not your gameplay logic at all. A device that does not meet a required limit, a shader that fails validation, a surface lost on resize or alt-tab, or an outright driver bug can panic or abort. These crashes cluster on specific GPUs and driver versions, so a player on an older integrated chip may fail at startup while everyone else runs fine.
Capturing the GPU adapter info, the chosen wgpu backend, and the driver version is therefore essential. Bevy can report the adapter name and backend, and including those in every crash report lets you see when a wave of startup panics all share one GPU family. Distinguishing a wgpu or driver fault from a gameplay panic is the first triage question for any Bevy crash, because the fixes are completely different: one is a fallback or limit adjustment, the other is your own logic. Context makes that distinction immediate.
Installing a panic hook
The foundation of Bevy crash reporting is std::panic::set_hook. Set it early in main, before the app runs, and your hook receives the PanicInfo with the message and the source location. Inside it, capture the panic message, the file and line, and a backtrace via std::backtrace::Backtrace::capture. Serialize that into a report and deliver it before the process exits. Because the hook runs on whatever thread panicked, keep its work minimal and resilient, since panicking inside a panic hook makes things worse.
Decide your unwinding strategy deliberately. Many Bevy games ship with panic equal to abort in release for smaller binaries and predictable behavior, which means your hook must finish reporting before abort fires. If you keep unwinding, you have a little more room but must still treat the app as doomed. Pair the hook with a maintained breadcrumb of recent state, the active app state or level, and the last few significant actions, so the report carries context the panic message alone never includes.
Setting it up with Bugnet
Bugnet fits Bevy because the panic hook is a single, clean integration point. In your set_hook closure, hand Bugnet the panic message, the source location, the captured backtrace, and your context, including the GPU adapter and backend and the active app state. The crash arrives as a readable Rust backtrace with the system and line identified, plus the hardware fields that separate a gameplay unwrap from a wgpu fault. You can also add an in-game report button from a Bevy UI system so players can flag non-fatal issues while the current state is attached automatically.
On the dashboard, Bugnet groups identical panics into one issue with an occurrence count, so the same unwrap on a None across many players is a single prioritized item instead of a scattered pile. Custom fields for GPU adapter, wgpu backend, and Bevy version let you filter at a glance: if startup panics only hit one Vulkan driver, or a logic panic spans all platforms, the grouped view makes it plain. That is what lets a small Rust team triage by impact rather than by whoever shouted loudest.
Designing for fewer panics
The best crash report is the one you never need because the panic never happened. Lean on Rust's type system: prefer matching on Option and Result over unwrap and expect in shipping code, use get for fallible queries, and handle the None or Err path with a graceful continue or a logged warning. Reserve expect for true invariants that should never fail, and give those expects descriptive messages so that if one does fire, the report explains the broken assumption. This discipline turns whole classes of panic into ordinary control flow.
Before release, enable backtraces in your shipping configuration, then deliberately panic from a test system and from a forced wgpu error, and confirm both reach your dashboard with backtrace and GPU context intact. Test on at least one weak or older GPU, since that is where wgpu faults hide. With a panic hook proven across hardware and panics grouped by signature, a Bevy project stays tractable as it grows: you ship, watch occurrence counts, and spend your scarce time on the panic affecting the most players.
Bevy crashes are panics, not exceptions. Hook them, capture the backtrace and GPU adapter, and let grouping tell a wgpu driver fault apart from your own unwrap.