Browse Source

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>
Matt Hunzinger 11 months ago
parent
commit
d07e81005f

+ 1 - 1
packages/core/src/error_boundary.rs

@@ -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)
             }

+ 3 - 3
packages/core/src/events.rs

@@ -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")
         }

+ 1 - 1
packages/core/src/generational_box.rs

@@ -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()
     }
 }

+ 33 - 23
packages/core/src/global_context.rs

@@ -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

+ 2 - 0
packages/core/src/properties.rs

@@ -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

+ 82 - 8
packages/core/src/runtime.rs

@@ -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 {}

+ 5 - 5
packages/core/src/scope_arena.rs

@@ -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 = {

+ 26 - 17
packages/core/src/scope_context.rs

@@ -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.

+ 1 - 0
packages/core/src/scopes.rs

@@ -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))
             {

+ 1 - 1
packages/core/src/suspense/mod.rs

@@ -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(),
         }
     }

+ 9 - 3
packages/core/src/tasks.rs

@@ -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();
 

+ 1 - 1
packages/signals/src/global/signal.rs

@@ -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)