Better expect error messages (#2629)

* Replace expect error messages

* Change message

* Create RuntimeError struct

* Pass error through core methods

* Fix use of Runtime::current in signals package

* Fix tests

* Add #[track_caller] for better error output and fix maybe_with_rt

* provide a help message along with RuntimeError

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
Matt Hunzinger 2024-07-24 14:31:56 -04:00 committed by GitHub
parent 9167cd9dec
commit d07e81005f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 165 additions and 63 deletions

View file

@ -167,7 +167,7 @@ where
error.context.push(Rc::new(AdditionalErrorContext {
backtrace: Backtrace::capture(),
context: Box::new(context()),
scope: current_scope_id(),
scope: current_scope_id().ok(),
}));
Err(error)
}

View file

@ -397,7 +397,7 @@ impl<Args: 'static, Ret: 'static> Callback<Args, Ret> {
));
Self {
callback,
origin: current_scope_id().expect("to be in a dioxus runtime"),
origin: current_scope_id().unwrap(),
}
}
@ -409,7 +409,7 @@ impl<Args: 'static, Ret: 'static> Callback<Args, Ret> {
as Rc<RefCell<dyn FnMut(Args) -> Ret>>));
Self {
callback,
origin: current_scope_id().expect("to be in a dioxus runtime"),
origin: current_scope_id().unwrap(),
}
}
@ -427,7 +427,7 @@ impl<Args: 'static, Ret: 'static> Callback<Args, Ret> {
value
})
})
.expect("Callback must be called from within the dioxus runtime")
.unwrap()
} else {
panic!("Callback was manually dropped")
}

View file

@ -91,6 +91,6 @@ pub fn current_owner<S: AnyStorage>() -> Owner<S> {
impl ScopeId {
/// Get the owner for the current scope.
pub fn owner<S: AnyStorage>(self) -> Owner<S> {
Runtime::with_scope(self, |cx| cx.owner::<S>()).expect("to be in a dioxus runtime")
Runtime::with_scope(self, |cx| cx.owner::<S>()).unwrap()
}
}

View file

@ -1,10 +1,14 @@
use crate::runtime::RuntimeError;
use crate::{innerlude::SuspendedFuture, runtime::Runtime, CapturedError, Element, ScopeId, Task};
use std::future::Future;
use std::sync::Arc;
/// Get the current scope id
pub fn current_scope_id() -> Option<ScopeId> {
Runtime::with(|rt| rt.current_scope_id()).flatten()
pub fn current_scope_id() -> Result<ScopeId, RuntimeError> {
Runtime::with(|rt| rt.current_scope_id().ok())
.ok()
.flatten()
.ok_or(RuntimeError::new())
}
#[doc(hidden)]
@ -31,19 +35,20 @@ pub fn vdom_is_rendering() -> bool {
/// }
/// ```
pub fn throw_error(error: impl Into<CapturedError> + 'static) {
current_scope_id()
.expect("to be in a dioxus runtime")
.throw_error(error)
current_scope_id().unwrap().throw_error(error)
}
/// Consume context from the current scope
pub fn try_consume_context<T: 'static + Clone>() -> Option<T> {
Runtime::with_current_scope(|cx| cx.consume_context::<T>()).flatten()
Runtime::with_current_scope(|cx| cx.consume_context::<T>())
.ok()
.flatten()
}
/// Consume context from the current scope
pub fn consume_context<T: 'static + Clone>() -> T {
Runtime::with_current_scope(|cx| cx.consume_context::<T>())
.ok()
.flatten()
.unwrap_or_else(|| panic!("Could not find context {}", std::any::type_name::<T>()))
}
@ -54,23 +59,25 @@ pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Opti
rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>())
})
.ok()
.flatten()
}
/// Check if the current scope has a context
pub fn has_context<T: 'static + Clone>() -> Option<T> {
Runtime::with_current_scope(|cx| cx.has_context::<T>()).flatten()
Runtime::with_current_scope(|cx| cx.has_context::<T>())
.ok()
.flatten()
}
/// Provide context to the current scope
pub fn provide_context<T: 'static + Clone>(value: T) -> T {
Runtime::with_current_scope(|cx| cx.provide_context(value)).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.provide_context(value)).unwrap()
}
/// Provide a context to the root scope
pub fn provide_root_context<T: 'static + Clone>(value: T) -> T {
Runtime::with_current_scope(|cx| cx.provide_root_context(value))
.expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.provide_root_context(value)).unwrap()
}
/// Suspended the current component on a specific task and then return None
@ -108,7 +115,7 @@ pub fn suspend(task: Task) -> Element {
///
#[doc = include_str!("../docs/common_spawn_errors.md")]
pub fn spawn_isomorphic(fut: impl Future<Output = ()> + 'static) -> Task {
Runtime::with_current_scope(|cx| cx.spawn_isomorphic(fut)).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.spawn_isomorphic(fut)).unwrap()
}
/// Spawns the future but does not return the [`Task`]. This task will automatically be canceled when the component is dropped.
@ -134,12 +141,12 @@ pub fn spawn_isomorphic(fut: impl Future<Output = ()> + 'static) -> Task {
///
#[doc = include_str!("../docs/common_spawn_errors.md")]
pub fn spawn(fut: impl Future<Output = ()> + 'static) -> Task {
Runtime::with_current_scope(|cx| cx.spawn(fut)).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.spawn(fut)).unwrap()
}
/// Queue an effect to run after the next render. You generally shouldn't need to interact with this function directly. [use_effect](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/fn.use_effect.html) will call this function for you.
pub fn queue_effect(f: impl FnOnce() + 'static) {
Runtime::with_current_scope(|cx| cx.queue_effect(f)).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.queue_effect(f)).unwrap()
}
/// Spawn a future that Dioxus won't clean up when this component is unmounted
@ -195,7 +202,7 @@ pub fn queue_effect(f: impl FnOnce() + 'static) {
///
#[doc = include_str!("../docs/common_spawn_errors.md")]
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<Task> {
Runtime::with_scope(ScopeId::ROOT, |cx| cx.spawn(fut))
Runtime::with_scope(ScopeId::ROOT, |cx| cx.spawn(fut)).ok()
}
/// Informs the scheduler that this task is no longer needed and should be removed.
@ -248,30 +255,33 @@ pub fn remove_future(id: Task) {
/// })
/// }
/// ```
#[track_caller]
pub fn use_hook<State: Clone + 'static>(initializer: impl FnOnce() -> State) -> State {
Runtime::with_current_scope(|cx| cx.use_hook(initializer)).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.use_hook(initializer)).unwrap()
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
pub fn generation() -> usize {
Runtime::with_current_scope(|cx| cx.generation()).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.generation()).unwrap()
}
/// Get the parent of the current scope if it exists
pub fn parent_scope() -> Option<ScopeId> {
Runtime::with_current_scope(|cx| cx.parent_id()).flatten()
Runtime::with_current_scope(|cx| cx.parent_id())
.ok()
.flatten()
}
/// Mark the current scope as dirty, causing it to re-render
pub fn needs_update() {
Runtime::with_current_scope(|cx| cx.needs_update());
let _ = Runtime::with_current_scope(|cx| cx.needs_update());
}
/// Mark the current scope as dirty, causing it to re-render
pub fn needs_update_any(id: ScopeId) {
Runtime::with_current_scope(|cx| cx.needs_update_any(id));
let _ = Runtime::with_current_scope(|cx| cx.needs_update_any(id));
}
/// Schedule an update for the current component
@ -280,7 +290,7 @@ pub fn needs_update_any(id: ScopeId) {
///
/// You should prefer [`schedule_update_any`] if you need to update multiple components.
pub fn schedule_update() -> Arc<dyn Fn() + Send + Sync> {
Runtime::with_current_scope(|cx| cx.schedule_update()).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.schedule_update()).unwrap()
}
/// Schedule an update for any component given its [`ScopeId`].
@ -289,7 +299,7 @@ pub fn schedule_update() -> Arc<dyn Fn() + Send + Sync> {
///
/// Note: Unlike [`needs_update`], the function returned by this method will work outside of the dioxus runtime.
pub fn schedule_update_any() -> Arc<dyn Fn(ScopeId) + Send + Sync> {
Runtime::with_current_scope(|cx| cx.schedule_update_any()).expect("to be in a dioxus runtime")
Runtime::with_current_scope(|cx| cx.schedule_update_any()).unwrap()
}
/// Creates a callback that will be run before the component is removed.
@ -393,12 +403,12 @@ pub fn use_after_render(f: impl FnMut() + 'static) {
/// This is a hook and will always run, so you can't unschedule it
/// Will run for every progression of suspense, though this might change in the future
pub fn before_render(f: impl FnMut() + 'static) {
Runtime::with_current_scope(|cx| cx.push_before_render(f));
let _ = Runtime::with_current_scope(|cx| cx.push_before_render(f));
}
/// Push a function to be run after the render is complete, even if it didn't complete successfully
pub fn after_render(f: impl FnMut() + 'static) {
Runtime::with_current_scope(|cx| cx.push_after_render(f));
let _ = Runtime::with_current_scope(|cx| cx.push_after_render(f));
}
/// Use a hook with a cleanup function

View file

@ -130,9 +130,11 @@ pub fn verify_component_called_as_component<C: ComponentFunction<P, M>, P, M>(co
}
let component_name = Runtime::with(|rt| {
current_scope_id()
.ok()
.and_then(|id| rt.get_state(id))
.map(|scope| scope.name)
})
.ok()
.flatten();
// If we are in a component, and the type name is the same as the active component name, then we can just return

View file

@ -8,6 +8,7 @@ use crate::{
};
use slotmap::DefaultKey;
use std::collections::BTreeSet;
use std::fmt;
use std::{
cell::{Cell, Ref, RefCell},
rc::Rc,
@ -66,8 +67,10 @@ impl Runtime {
}
/// Get the current runtime
pub fn current() -> Option<Rc<Self>> {
RUNTIMES.with(|stack| stack.borrow().last().cloned())
pub fn current() -> Result<Rc<Self>, RuntimeError> {
RUNTIMES
.with(|stack| stack.borrow().last().cloned())
.ok_or(RuntimeError::new())
}
/// Create a scope context. This slab is synchronized with the scope slab.
@ -106,8 +109,12 @@ impl Runtime {
}
/// Get the current scope id
pub(crate) fn current_scope_id(&self) -> Option<ScopeId> {
self.scope_stack.borrow().last().copied()
pub(crate) fn current_scope_id(&self) -> Result<ScopeId, RuntimeError> {
self.scope_stack
.borrow()
.last()
.copied()
.ok_or(RuntimeError { _priv: () })
}
/// Call this function with the current scope set to the given scope
@ -190,22 +197,31 @@ impl Runtime {
}
/// Runs a function with the current runtime
pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Option<R> {
pub(crate) fn with<R>(f: impl FnOnce(&Runtime) -> R) -> Result<R, RuntimeError> {
Self::current().map(|r| f(&r))
}
/// Runs a function with the current scope
pub(crate) fn with_current_scope<R>(f: impl FnOnce(&Scope) -> R) -> Option<R> {
pub(crate) fn with_current_scope<R>(f: impl FnOnce(&Scope) -> R) -> Result<R, RuntimeError> {
Self::with(|rt| {
rt.current_scope_id()
.ok()
.and_then(|scope| rt.get_state(scope).map(|sc| f(&sc)))
})
.ok()
.flatten()
.ok_or(RuntimeError::new())
}
/// Runs a function with the current scope
pub(crate) fn with_scope<R>(scope: ScopeId, f: impl FnOnce(&Scope) -> R) -> Option<R> {
Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc))).flatten()
pub(crate) fn with_scope<R>(
scope: ScopeId,
f: impl FnOnce(&Scope) -> R,
) -> Result<R, RuntimeError> {
Self::with(|rt| rt.get_state(scope).map(|sc| f(&sc)))
.ok()
.flatten()
.ok_or(RuntimeError::new())
}
/// Finish a render. This will mark all effects as ready to run and send the render signal.
@ -279,3 +295,61 @@ impl Drop for RuntimeGuard {
Runtime::pop();
}
}
/// Missing Dioxus runtime error.
pub struct RuntimeError {
_priv: (),
}
impl RuntimeError {
#[inline(always)]
pub(crate) fn new() -> Self {
Self { _priv: () }
}
}
impl fmt::Debug for RuntimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RuntimeError").finish()
}
}
impl fmt::Display for RuntimeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Must be called from inside a Dioxus runtime.
Help: Some APIs in dioxus require a global runtime to be present.
If you are calling one of these APIs from outside of a dioxus runtime
(typically in a web-sys closure or dynamic library), you will need to
grab the runtime from a scope that has it and then move it into your
new scope with a runtime guard.
For example, if you are trying to use dioxus apis from a web-sys
closure, you can grab the runtime from the scope it is created in:
```rust
use dioxus::prelude::*;
static COUNT: GlobalSignal<i32> = Signal::global(|| 0);
#[component]
fn MyComponent() -> Element {
use_effect(|| {
// Grab the runtime from the MyComponent scope
let runtime = Runtime::current().expect(\"Components run in the Dioxus runtime\");
// Move the runtime into the web-sys closure scope
let web_sys_closure = Closure::new(|| {
// Then create a guard to provide the runtime to the closure
let _guard = RuntimeGuard::new(runtime);
// and run whatever code needs the runtime
tracing::info!(\"The count is: {COUNT}\");
});
})
}
```"
)
}
}
impl std::error::Error for RuntimeError {}

View file

@ -16,7 +16,7 @@ impl VirtualDom {
props: BoxedAnyProps,
name: &'static str,
) -> &mut ScopeState {
let parent_id = self.runtime.current_scope_id();
let parent_id = self.runtime.current_scope_id().ok();
let height = match parent_id.and_then(|id| self.runtime.get_state(id)) {
Some(parent) => parent.height() + 1,
None => 0,
@ -47,11 +47,11 @@ impl VirtualDom {
/// Run a scope and return the rendered nodes. This will not modify the DOM or update the last rendered node of the scope.
#[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")]
#[track_caller]
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn {
debug_assert!(
crate::Runtime::current().is_some(),
"Must be in a dioxus runtime"
);
// Ensure we are currently inside a `Runtime`.
crate::Runtime::current().unwrap();
self.runtime.clone().with_scope_on_stack(scope_id, || {
let scope = &self.scopes[scope_id.0];
let output = {

View file

@ -1,3 +1,4 @@
use crate::runtime::RuntimeError;
use crate::{innerlude::SchedulerMsg, Runtime, ScopeId, Task};
use crate::{
innerlude::{throw_into, CapturedError},
@ -219,7 +220,7 @@ impl Scope {
None
});
match cur_runtime.flatten() {
match cur_runtime.ok().flatten() {
Some(ctx) => Some(ctx),
None => {
tracing::trace!(
@ -450,13 +451,18 @@ impl Scope {
impl ScopeId {
/// Get the current scope id
pub fn current_scope_id(self) -> Option<ScopeId> {
Runtime::with(|rt| rt.current_scope_id()).flatten()
pub fn current_scope_id(self) -> Result<ScopeId, RuntimeError> {
Runtime::with(|rt| rt.current_scope_id().ok())
.ok()
.flatten()
.ok_or(RuntimeError::new())
}
/// Consume context from the current scope
pub fn consume_context<T: 'static + Clone>(self) -> Option<T> {
Runtime::with_scope(self, |cx| cx.consume_context::<T>()).flatten()
Runtime::with_scope(self, |cx| cx.consume_context::<T>())
.ok()
.flatten()
}
/// Consume context from the current scope
@ -465,64 +471,67 @@ impl ScopeId {
rt.get_state(scope_id)
.and_then(|cx| cx.consume_context::<T>())
})
.ok()
.flatten()
}
/// Check if the current scope has a context
pub fn has_context<T: 'static + Clone>(self) -> Option<T> {
Runtime::with_scope(self, |cx| cx.has_context::<T>()).flatten()
Runtime::with_scope(self, |cx| cx.has_context::<T>())
.ok()
.flatten()
}
/// Provide context to the current scope
pub fn provide_context<T: 'static + Clone>(self, value: T) -> T {
Runtime::with_scope(self, |cx| cx.provide_context(value))
.expect("to be in a dioxus runtime")
Runtime::with_scope(self, |cx| cx.provide_context(value)).unwrap()
}
/// Pushes the future onto the poll queue to be polled after the component renders.
pub fn push_future(self, fut: impl Future<Output = ()> + 'static) -> Option<Task> {
Runtime::with_scope(self, |cx| cx.spawn(fut))
Runtime::with_scope(self, |cx| cx.spawn(fut)).ok()
}
/// Spawns the future but does not return the [`Task`]
pub fn spawn(self, fut: impl Future<Output = ()> + 'static) {
Runtime::with_scope(self, |cx| cx.spawn(fut));
Runtime::with_scope(self, |cx| cx.spawn(fut)).unwrap();
}
/// Get the current render since the inception of this component
///
/// This can be used as a helpful diagnostic when debugging hooks/renders, etc
pub fn generation(self) -> Option<usize> {
Runtime::with_scope(self, |cx| Some(cx.generation())).expect("to be in a dioxus runtime")
Runtime::with_scope(self, |cx| Some(cx.generation())).unwrap()
}
/// Get the parent of the current scope if it exists
pub fn parent_scope(self) -> Option<ScopeId> {
Runtime::with_scope(self, |cx| cx.parent_id()).flatten()
Runtime::with_scope(self, |cx| cx.parent_id())
.ok()
.flatten()
}
/// Mark the current scope as dirty, causing it to re-render
pub fn needs_update(self) {
Runtime::with_scope(self, |cx| cx.needs_update());
Runtime::with_scope(self, |cx| cx.needs_update()).unwrap();
}
/// Create a subscription that schedules a future render for the reference component. Unlike [`Self::needs_update`], this function will work outside of the dioxus runtime.
///
/// ## Notice: you should prefer using [`crate::prelude::schedule_update_any`]
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
Runtime::with_scope(*self, |cx| cx.schedule_update()).expect("to be in a dioxus runtime")
Runtime::with_scope(*self, |cx| cx.schedule_update()).unwrap()
}
/// Get the height of the current scope
pub fn height(self) -> u32 {
Runtime::with_scope(self, |cx| cx.height()).expect("to be in a dioxus runtime")
Runtime::with_scope(self, |cx| cx.height()).unwrap()
}
/// Run a closure inside of scope's runtime
#[track_caller]
pub fn in_runtime<T>(self, f: impl FnOnce() -> T) -> T {
Runtime::current()
.expect("to be in a dioxus runtime")
.on_scope(self, f)
Runtime::current().unwrap().on_scope(self, f)
}
/// Throw a [`CapturedError`] into a scope. The error will bubble up to the nearest [`ErrorBoundary`] or the root of the app.

View file

@ -21,6 +21,7 @@ impl std::fmt::Debug for ScopeId {
#[cfg(debug_assertions)]
{
if let Some(name) = Runtime::current()
.ok()
.as_ref()
.and_then(|rt| rt.get_state(*self))
{

View file

@ -46,7 +46,7 @@ impl SuspendedFuture {
pub fn new(task: Task) -> Self {
Self {
task,
origin: current_scope_id().expect("to be in a dioxus runtime"),
origin: current_scope_id().unwrap(),
placeholder: VNode::placeholder(),
}
}

View file

@ -81,7 +81,8 @@ impl Task {
_ = rt
.sender
.unbounded_send(SchedulerMsg::TaskNotified(self.id))
});
})
.unwrap();
}
/// Poll the task immediately.
@ -100,7 +101,8 @@ impl Task {
.unbounded_send(SchedulerMsg::TaskNotified(self.id));
}
}
});
})
.unwrap();
}
}
@ -246,7 +248,11 @@ impl Runtime {
}
pub(crate) fn handle_task_wakeup(&self, id: Task) -> Poll<()> {
debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime");
#[cfg(feature = "debug_assertions")]
{
// Ensure we are currently inside a `Runtime`.
Runtime::current().unwrap();
}
let task = self.tasks.borrow().get(id.id).cloned();

View file

@ -68,7 +68,7 @@ impl<T: 'static> GlobalSignal<T> {
#[doc(hidden)]
pub fn maybe_with_rt<O>(&self, f: impl FnOnce(&T) -> O) -> O {
if Runtime::current().is_none() {
if Runtime::current().is_err() {
f(&(self.initializer)())
} else {
self.with(f)