Repository: smol-rs/async-task (language: rust)

Issue:
Bug: Unit test with expected panic message randomly fails because of crate-specific panic message
I currently write unit tests for my Bevy project which uses the current `async-task` version.
My unit test looks like this:
```rs
#[test]
#[should_panic = "invalid `progress_query`: `QueryOutOfRange(2..=3)`"]
```

As expected, the test fails with the given message in one of the systems I wrote. However, the unit test _sometimes_ still fails because my panic seems to cause another panic here with a different message text:

https://github.com/smol-rs/async-task/blob/230b0a4c842b57c734c4c2f1e965d190b670a9b3/src/task.rs#L426

> ---- controller::test::forward::forward_query_forward_to_too_many_ticks stdout ----
note: panic did not contain expected string
      panic message: `"task has failed"`,
 expected substring: `"invalid `progress_query`: `QueryOutOfRange(2..=3)`"`

In my case the `async_task` code is called by bevy here:

https://github.com/bevyengine/bevy/blob/93a131661de507eb711264b11965fe1d4bb13f12/crates/bevy_tasks/src/task_pool.rs#L173

It only happens sometimes though, I can repeat the test multiple times (without recompiling) and this happens only sometimes. Seems to be a race condition.

This is the full console output. My project is rather large so I could not set up a minimal example of this issue yet.

<details>
<summary>Hidden output</summary>
<br>
     Running unittests src\lib.rs (target\debug\deps\library-55a25fc12b677daa.exe)

running 1 test
thread 'TaskPool (0)' panicked at 'invalid `progress_query`: `QueryOutOfRange(2..=3)`', src\controller\test.rs:163:25
stack backtrace:
   0: std::panicking::begin_panic_handler
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\std\src\panicking.rs:556
   1: core::panicking::panic_fmt
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\core\src\panicking.rs:142
   2: library::controller::test::impl$3::run::closure$1<2>
             at .\src\controller\test.rs:163
   3: core::ops::function::impls::impl$3::call_mut<tuple$<bevy_ecs::system::system_param::Res<library::controller::Controller>,bevy_ecs::system::commands::Commands>,library::controller::test::impl$3::run::closure_env$1<2> >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\ops\function.rs:297
   4: bevy_ecs::system::function_system::impl$18::run::call_inner<tuple$<>,bevy_ecs::system::system_param::Res<library::controller::Controller>,bevy_ecs::system::commands::Commands,ref_mut$<library::controller::test::impl$3::run::closure_env$1<2> > >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\system\function_system.rs:564   
   5: bevy_ecs::system::function_system::impl$18::run<tuple$<>,library::controller::test::impl$3::run::closure_env$1<2>,bevy_ecs::system::system_param::Res<library::controller::Controller>,bevy_ecs::system::commands::Commands>
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\system\function_system.rs:567   
   6: bevy_ecs::system::function_system::impl$6::run_unsafe<tuple$<>,tuple$<>,tuple$<bevy_ecs::system::system_param::Res<library::controller::Controller>,bevy_ecs::system::commands::Commands>,tuple$<>,library::controller::test::impl$3::run::closure_env$1<2> >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\system\function_system.rs:403   
   7: bevy_ecs::schedule::executor_parallel::impl$2::prepare_systems::async_block$0
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\schedule\executor_parallel.rs:194
   8: core::future::from_generator::impl$1::poll<enum2$<bevy_ecs::schedule::executor_parallel::impl$2::prepare_systems::async_block_env$0> >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\future\mod.rs:91
   9: async_executor::impl$4::spawn::async_block$0<tuple$<>,core::future::from_generator::GenFuture<enum2$<bevy_ecs::schedule::executor_parallel::impl$2::prepare_systems::async_block_env$0> > >        
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-executor-1.4.1\src\lib.rs:144
  10: core::future::from_generator::impl$1::poll<enum2$<async_executor::impl$4::spawn::async_block_env$0<tuple$<>,core::future::from_generator::GenFuture<enum2$<bevy_ecs::schedule::executor_parallel::impl$2::prepare_systems::async_block_env$0> > > > >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\future\mod.rs:91
  11: async_task::raw::RawTask<core::future::from_generator::GenFuture<enum2$<async_executor::impl$4::spawn::async_block_env$0<tuple$<>,core::future::from_generator::GenFuture<enum2$<bevy_ecs::schedule::executor_parallel::impl$2::prepare_systems::async_block_en       
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-task-4.3.0\src\raw.rs:511
  12: async_task::runnable::Runnable::run
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-task-4.3.0\src\runnable.rs:309
  13: async_executor::impl$4::run::async_fn$0::async_block$0<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,async_channel::Recv<tuple$<> > >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-executor-1.4.1\src\lib.rs:235
  14: core::future::from_generator::impl$1::poll<enum2$<async_executor::impl$4::run::async_fn$0::async_block_env$0<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,async_channel::Recv<tuple$<> > > > >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\future\mod.rs:91
  15: futures_lite::future::impl$12::poll<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,async_channel::Recv<tuple$<> >,core::future::from_generator::GenFuture<enum2$<async_executor::impl$4::run::async_fn$0::async_block_env$0<enum2$<core::resu       
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\futures-lite-1.12.0\src\future.rs:529
  16: async_executor::impl$4::run::async_fn$0<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,async_channel::Recv<tuple$<> > >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-executor-1.4.1\src\lib.rs:242
  17: core::future::from_generator::impl$1::poll<enum2$<async_executor::impl$4::run::async_fn_env$0<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,async_channel::Recv<tuple$<> > > > >             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\future\mod.rs:91
  18: futures_lite::future::block_on::closure$0<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,core::future::from_generator::GenFuture<enum2$<async_executor::impl$4::run::async_fn_env$0<enum2$<core::result::Result<tuple$<>,async_channel::RecvE       
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\futures-lite-1.12.0\src\future.rs:89
  19: std::thread::local::LocalKey<core::cell::RefCell<tuple$<parking::Parker,core::task::wake::Waker> > >::try_with<core::cell::RefCell<tuple$<parking::Parker,core::task::wake::Waker> >,futures_lite::future::block_on::closure_env$0<enum2$<core::result::Result<       
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\std\src\thread\local.rs:446
  20: std::thread::local::LocalKey<core::cell::RefCell<tuple$<parking::Parker,core::task::wake::Waker> > >::with<core::cell::RefCell<tuple$<parking::Parker,core::task::wake::Waker> >,futures_lite::future::block_on::closure_env$0<enum2$<core::result::Result<tupl       
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\std\src\thread\local.rs:422
  21: futures_lite::future::block_on<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,core::future::from_generator::GenFuture<enum2$<async_executor::impl$4::run::async_fn_env$0<enum2$<core::result::Result<tuple$<>,async_channel::RecvError> >,asy       
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\futures-lite-1.12.0\src\future.rs:79
  22: bevy_tasks::task_pool::impl$1::new_internal::closure$0::closure$0
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_tasks-0.8.1\src\task_pool.rs:120
note: Some details are omitted, run with `RUST_BACKTRACE=full` for 
a verbose backtrace.
thread 'controller::test::forward::forward_query_forward_to_too_many_ticks' panicked at 'task has failed', C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-task-4.3.0\src\task.rs:426:45
stack backtrace:
   0: std::panicking::begin_panic_handler
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\std\src\panicking.rs:556
   1: core::panicking::panic_fmt
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\core\src\panicking.rs:142
   2: core::panicking::panic_display<str>
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\core\src\panicking.rs:72
   3: core::panicking::panic_str
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\core\src\panicking.rs:56
   4: core::option::expect_failed
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\core\src\option.rs:1873
   5: enum2$<core::option::Option<tuple$<> > >::expect<tuple$<> >  
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\option.rs:738
   6: async_task::task::impl$7::poll<tuple$<> >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-task-4.3.0\src\task.rs:426
   7: bevy_tasks::task_pool::impl$1::scope::closure$0::async_block$0<bevy_ecs::schedule::executor_parallel::impl$1::run_systems::closure_env$0,tuple$<> >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_tasks-0.8.1\src\task_pool.rs:173
   8: core::future::from_generator::impl$1::poll<enum2$<bevy_tasks::task_pool::impl$1::scope::closure$0::async_block_env$0<bevy_ecs::schedule::executor_parallel::impl$1::run_systems::closure_env$0,tuple$<> > > >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\future\mod.rs:91
   9: core::future::future::impl$1::poll<ref_mut$<dyn$<core::future::future::Future<assoc$<Output,alloc::vec::Vec<tuple$<>,alloc::alloc::Global> > > > > >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\future\future.rs:124
  10: async_executor::impl$9::spawn::async_block$0<alloc::vec::Vec<tuple$<>,alloc::alloc::Global>,core::pin::Pin<ref_mut$<dyn$<core::future::future::Future<assoc$<Output,alloc::vec::Vec<tuple$<>,alloc::alloc::Global> > > > > > >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-executor-1.4.1\src\lib.rs:376
  11: core::future::from_generator::impl$1::poll<enum2$<async_executor::impl$9::spawn::async_block_env$0<alloc::vec::Vec<tuple$<>,alloc::alloc::Global>,core::pin::Pin<ref_mut$<dyn$<core::future::future::Future<assoc$<Output,alloc::vec::Vec<tuple$<>,alloc::alloc       
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\future\mod.rs:91
  12: async_task::raw::RawTask<core::future::from_generator::GenFuture<enum2$<async_executor::impl$9::spawn::async_block_env$0<alloc::vec::Vec<tuple$<>,alloc::alloc::Global>,core::pin::Pin<ref_mut$<dyn$<core::future::future::Future<assoc$<Output,alloc::vec::Vec       
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-task-4.3.0\src\raw.rs:511
  13: async_task::runnable::Runnable::run
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-task-4.3.0\src\runnable.rs:309
  14: async_executor::Executor::try_tick
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-executor-1.4.1\src\lib.rs:181
  15: async_executor::LocalExecutor::try_tick
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\async-executor-1.4.1\src\lib.rs:405
  16: bevy_tasks::task_pool::impl$1::scope::closure$0<bevy_ecs::schedule::executor_parallel::impl$1::run_systems::closure_env$0,tuple$<> >
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_tasks-0.8.1\src\task_pool.rs:202
  17: std::thread::local::LocalKey<async_executor::LocalExecutor>::try_with<async_executor::LocalExecutor,bevy_tasks::task_pool::impl$1::scope::closure_env$0<bevy_ecs::schedule::executor_parallel::impl$1::run_systems::closure_env$0,tuple$<> >,alloc::vec::Vec<tu       
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\std\src\thread\local.rs:446
  18: std::thread::local::LocalKey<async_executor::LocalExecutor>::with<async_executor::LocalExecutor,bevy_tasks::task_pool::impl$1::scope::closure_env$0<bevy_ecs::schedule::executor_parallel::impl$1::run_systems::closure_env$0,tuple$<> >,alloc::vec::Vec<tuple$       
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\std\src\thread\local.rs:422
  19: bevy_tasks::task_pool::TaskPool::scope<bevy_ecs::schedule::executor_parallel::impl$1::run_systems::closure_env$0,tuple$<> >     
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_tasks-0.8.1\src\task_pool.rs:148
  20: bevy_ecs::schedule::executor_parallel::impl$1::run_systems   
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\schedule\executor_parallel.rs:126
  21: bevy_ecs::schedule::stage::impl$1::run
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\schedule\stage.rs:884
  22: bevy_ecs::schedule::Schedule::run_once
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\schedule\mod.rs:342
  23: bevy_ecs::schedule::impl$1::run
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_ecs-0.8.1\src\schedule\mod.rs:360
  24: bevy_app::app::App::update
             at C:\Users\Anonymous\.cargo\registry\src\github.com-1ecc6299db9ec823\bevy_app-0.8.1\src\app.rs:119
  25: library::controller::test::impl$3::run::closure$4<2>
             at .\src\controller\test.rs:208
  26: core::iter::traits::iterator::Iterator::for_each::call::closure$0<usize,library::controller::test::impl$3::run::closure_env$4<2> >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\iter\traits\iterator.rs:828
  27: core::iter::traits::iterator::Iterator::fold<core::ops::range::Range<usize>,tuple$<>,core::iter::traits::iterator::Iterator::for_each::call::closure_env$0<usize,library::controller::test::impl$3::run::closure_env$4<2> > >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\iter\traits\iterator.rs:2414
  28: core::iter::traits::iterator::Iterator::for_each<core::ops::range::Range<usize>,library::controller::test::impl$3::run::closure_env$4<2> >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\iter\traits\iterator.rs:831
  29: library::controller::test::impl$3::run<2>
             at .\src\controller\test.rs:208
  30: library::controller::test::forward::forward_query_forward_to_too_many_ticks
             at .\src\controller\test\forward.rs:94
  31: library::controller::test::forward::forward_query_forward_to_too_many_ticks::closure$0
             at .\src\controller\test\forward.rs:93
  32: core::ops::function::FnOnce::call_once<library::controller::test::forward::forward_query_forward_to_too_many_ticks::closure_env$0,tuple$<> >
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c\library\core\src\ops\function.rs:251
  33: core::ops::function::FnOnce::call_once
             at /rustc/57f097ea25f2c05f424fc9b9dc50dbd6d399845c/library\core\src\ops\function.rs:251
note: Some details are omitted, run with `RUST_BACKTRACE=full` for 
a verbose backtrace.
test controller::test::forward::forward_query_forward_to_too_many_ticks - should panic ... FAILED

failures:

---- controller::test::forward::forward_query_forward_to_too_many_ticks stdout ----
note: panic did not contain expected string
      panic message: `"task has failed"`,
 expected substring: `"invalid `progress_query`: `QueryOutOfRange(2..=3)`"`

failures:
    controller::test::forward::forward_query_forward_to_too_many_ticks

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 5 filtered out; finished in 0.60s
</details>



Discussion / hints:
~~At the moment, `async-task` doesn't handle panics beyond just propagating them up to whatever's running the task at the moment (most likely a thread in a thread pool). If you want to be able to propagate panics logically, you should file a feature request in the `async-task` repository.~~

I just realized that this *is* `async-task`. Do you want to be able to propagate panics?
Yes that was my intention so I am able to align my unittests to certain panic messages no matter which thread they happen in.

Personally I fixed the issue by changing my tests to not use `should_panic` but instead check the error in an assert inside the test. Still this might be worth investigating to support for other users.

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 8c611bf..ea06cf8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,7 @@ atomic-waker = "1"
 easy-parallel = "3"
 flaky_test = "0.1"
 flume = { version = "0.10", default-features = false }
+futures-lite = "1.12.0"
 once_cell = "1"
 smol = "1"
 
diff --git a/src/header.rs b/src/header.rs
index 9747c5d..3dd35e7 100644
--- a/src/header.rs
+++ b/src/header.rs
@@ -31,6 +31,10 @@ pub(crate) struct Header<M> {
     ///
     /// This metadata may be provided to the user.
     pub(crate) metadata: M,
+
+    /// Whether or not a panic that occurs in the task should be propagated.
+    #[cfg(feature = "std")]
+    pub(crate) propagate_panic: bool,
 }
 
 impl<M> Header<M> {
diff --git a/src/raw.rs b/src/raw.rs
index 4af7b27..4bba757 100644
--- a/src/raw.rs
+++ b/src/raw.rs
@@ -13,6 +13,12 @@ use crate::state::*;
 use crate::utils::{abort, abort_on_panic, max, Layout};
 use crate::Runnable;
 
+#[cfg(feature = "std")]
+pub(crate) type Panic = alloc::boxed::Box<dyn core::any::Any + Send + 'static>;
+
+#[cfg(not(feature = "std"))]
+pub(crate) type Panic = core::convert::Infallible;
+
 /// The vtable for a task.
 pub(crate) struct TaskVTable {
     /// Schedules the task.
@@ -76,7 +82,7 @@ pub(crate) struct RawTask<F, T, S, M> {
     pub(crate) future: *mut F,
 
     /// The output of the future.
-    pub(crate) output: *mut T,
+    pub(crate) output: *mut Result<T, Panic>,
 }
 
 impl<F, T, S, M> Copy for RawTask<F, T, S, M> {}
@@ -97,7 +103,7 @@ impl<F, T, S, M> RawTask<F, T, S, M> {
         let layout_header = Layout::new::<Header<M>>();
         let layout_s = Layout::new::<S>();
         let layout_f = Layout::new::<F>();
-        let layout_r = Layout::new::<T>();
+        let layout_r = Layout::new::<Result<T, Panic>>();
 
         // Compute the layout for `union { F, T }`.
         let size_union = max(layout_f.size(), layout_r.size());
@@ -138,7 +144,7 @@ where
     pub(crate) fn allocate<'a, Gen: FnOnce(&'a M) -> F>(
         future: Gen,
         schedule: S,
-        metadata: M,
+        builder: crate::Builder<M>,
     ) -> NonNull<()>
     where
         F: 'a,
@@ -158,6 +164,12 @@ where
 
             let raw = Self::from_ptr(ptr.as_ptr());
 
+            let crate::Builder {
+                metadata,
+                #[cfg(feature = "std")]
+                propagate_panic,
+            } = builder;
+
             // Write the header as the first field of the task.
             (raw.header as *mut Header<M>).write(Header {
                 state: AtomicUsize::new(SCHEDULED | TASK | REFERENCE),
@@ -173,6 +185,8 @@ where
                     layout_info: &Self::TASK_LAYOUT,
                 },
                 metadata,
+                #[cfg(feature = "std")]
+                propagate_panic,
             });
 
             // Write the schedule function as the third field of the task.
@@ -199,7 +213,7 @@ where
                 header: p as *const Header<M>,
                 schedule: p.add(task_layout.offset_s) as *const S,
                 future: p.add(task_layout.offset_f) as *mut F,
-                output: p.add(task_layout.offset_r) as *mut T,
+                output: p.add(task_layout.offset_r) as *mut Result<T, Panic>,
             }
         }
     }
@@ -525,8 +539,30 @@ where
 
         // Poll the inner future, but surround it with a guard that closes the task in case polling
         // panics.
+        // If available, we should also try to catch the panic so that it is propagated correctly.
         let guard = Guard(raw);
-        let poll = <F as Future>::poll(Pin::new_unchecked(&mut *raw.future), cx);
+
+        // Panic propagation is not available for no_std.
+        #[cfg(not(feature = "std"))]
+        let poll = <F as Future>::poll(Pin::new_unchecked(&mut *raw.future), cx).map(Ok);
+
+        #[cfg(feature = "std")]
+        let poll = {
+            // Check if we should propagate panics.
+            if (*raw.header).propagate_panic {
+                // Use catch_unwind to catch the panic.
+                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+                    <F as Future>::poll(Pin::new_unchecked(&mut *raw.future), cx)
+                })) {
+                    Ok(Poll::Ready(v)) => Poll::Ready(Ok(v)),
+                    Ok(Poll::Pending) => Poll::Pending,
+                    Err(e) => Poll::Ready(Err(e)),
+                }
+            } else {
+                <F as Future>::poll(Pin::new_unchecked(&mut *raw.future), cx).map(Ok)
+            }
+        };
+
         mem::forget(guard);
 
         match poll {
diff --git a/src/runnable.rs b/src/runnable.rs
index c10177d..e371176 100644
--- a/src/runnable.rs
+++ b/src/runnable.rs
@@ -17,7 +17,11 @@ use crate::Task;
 #[derive(Debug)]
 pub struct Builder<M> {
     /// The metadata associated with the task.
-    metadata: M,
+    pub(crate) metadata: M,
+
+    /// Whether or not a panic that occurs in the task should be propagated.
+    #[cfg(feature = "std")]
+    pub(crate) propagate_panic: bool,
 }
 
 impl<M: Default> Default for Builder<M> {
@@ -40,7 +44,11 @@ impl Builder<()> {
     /// let (runnable, task) = Builder::new().spawn(|()| async {}, |_| {});
     /// ```
     pub fn new() -> Builder<()> {
-        Builder { metadata: () }
+        Builder {
+            metadata: (),
+            #[cfg(feature = "std")]
+            propagate_panic: false,
+        }
     }
 
     /// Adds metadata to the task.
@@ -123,11 +131,63 @@ impl Builder<()> {
     /// # });
     /// ```
     pub fn metadata<M>(self, metadata: M) -> Builder<M> {
-        Builder { metadata }
+        Builder {
+            metadata,
+            #[cfg(feature = "std")]
+            propagate_panic: self.propagate_panic,
+        }
     }
 }
 
 impl<M> Builder<M> {
+    /// Propagates panics that occur in the task.
+    ///
+    /// When this is `true`, panics that occur in the task will be propagated to the caller of
+    /// the [`Task`]. When this is false, no special action is taken when a panic occurs in the
+    /// task, meaning that the caller of [`Runnable::run`] will observe a panic.
+    ///
+    /// This is only available when the `std` feature is enabled. By default, this is `false`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use async_task::Builder;
+    /// use futures_lite::future::poll_fn;
+    /// use std::future::Future;
+    /// use std::panic;
+    /// use std::pin::Pin;
+    /// use std::task::{Context, Poll};
+    ///
+    /// fn did_panic<F: FnOnce()>(f: F) -> bool {
+    ///     panic::catch_unwind(panic::AssertUnwindSafe(f)).is_err()
+    /// }
+    ///
+    /// # smol::future::block_on(async {
+    /// let (runnable1, mut task1) = Builder::new()
+    ///    .propagate_panic(true)
+    ///    .spawn(|()| async move { panic!() }, |_| {});
+    ///
+    /// let (runnable2, mut task2) = Builder::new()
+    ///    .propagate_panic(false)
+    ///    .spawn(|()| async move { panic!() }, |_| {});
+    ///
+    /// assert!(!did_panic(|| { runnable1.run(); }));
+    /// assert!(did_panic(|| { runnable2.run(); }));
+    ///
+    /// let waker = poll_fn(|cx| Poll::Ready(cx.waker().clone())).await;
+    /// let mut cx = Context::from_waker(&waker);
+    /// assert!(did_panic(|| { let _ = Pin::new(&mut task1).poll(&mut cx); }));
+    /// assert!(did_panic(|| { let _ = Pin::new(&mut task2).poll(&mut cx); }));
+    /// # });
+    /// ```
+    #[cfg(feature = "std")]
+    pub fn propagate_panic(self, propagate_panic: bool) -> Builder<M> {
+        Builder {
+            metadata: self.metadata,
+            propagate_panic,
+        }
+    }
+
     /// Creates a new task.
     ///
     /// The returned [`Runnable`] is used to poll the `future`, and the [`Task`] is used to await its
@@ -313,8 +373,6 @@ impl<M> Builder<M> {
         S: Fn(Runnable<M>),
         M: 'a,
     {
-        let Self { metadata } = self;
-
         // Allocate large futures on the heap.
         let ptr = if mem::size_of::<Fut>() >= 2048 {
             let future = |meta| {
@@ -322,9 +380,9 @@ impl<M> Builder<M> {
                 Box::pin(future)
             };
 
-            RawTask::<_, Fut::Output, S, M>::allocate(future, schedule, metadata)
+            RawTask::<_, Fut::Output, S, M>::allocate(future, schedule, self)
         } else {
-            RawTask::<Fut, Fut::Output, S, M>::allocate(future, schedule, metadata)
+            RawTask::<Fut, Fut::Output, S, M>::allocate(future, schedule, self)
         };
 
         let runnable = Runnable {
diff --git a/src/task.rs b/src/task.rs
index 49ba501..5bf8b46 100644
--- a/src/task.rs
+++ b/src/task.rs
@@ -8,6 +8,7 @@ use core::sync::atomic::Ordering;
 use core::task::{Context, Poll};
 
 use crate::header::Header;
+use crate::raw::Panic;
 use crate::state::*;
 
 /// A spawned task.
@@ -226,7 +227,7 @@ impl<T, M> Task<T, M> {
     }
 
     /// Puts the task in detached state.
-    fn set_detached(&mut self) -> Option<T> {
+    fn set_detached(&mut self) -> Option<Result<T, Panic>> {
         let ptr = self.ptr.as_ptr();
         let header = ptr as *const Header<M>;
 
@@ -256,8 +257,10 @@ impl<T, M> Task<T, M> {
                         ) {
                             Ok(_) => {
                                 // Read the output.
-                                output =
-                                    Some((((*header).vtable.get_output)(ptr) as *mut T).read());
+                                output = Some(
+                                    (((*header).vtable.get_output)(ptr) as *mut Result<T, Panic>)
+                                        .read(),
+                                );
 
                                 // Update the state variable because we're continuing the loop.
                                 state |= CLOSED;
@@ -382,8 +385,22 @@ impl<T, M> Task<T, M> {
                         }
 
                         // Take the output from the task.
-                        let output = ((*header).vtable.get_output)(ptr) as *mut T;
-                        return Poll::Ready(Some(output.read()));
+                        let output = ((*header).vtable.get_output)(ptr) as *mut Result<T, Panic>;
+                        let output = output.read();
+
+                        // Propagate the panic if the task panicked.
+                        let output = match output {
+                            Ok(output) => output,
+                            Err(panic) => {
+                                #[cfg(feature = "std")]
+                                std::panic::resume_unwind(panic);
+
+                                #[cfg(not(feature = "std"))]
+                                match panic {}
+                            }
+                        };
+
+                        return Poll::Ready(Some(output));
                     }
                     Err(s) => state = s,
                 }


Test patch: