diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 7d6a9d5a3..f5105d460 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -188,8 +188,8 @@ const noOp = () => {};
 
 let expandNotificationsController = new AbortController();
 
-export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
-  return (dispatch, getState) => {
+export function expandNotifications({ maxId = undefined, forceLoad = false }) {
+  return async (dispatch, getState) => {
     const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
     const notifications = getState().get('notifications');
     const isLoadingMore = !!maxId;
@@ -199,7 +199,6 @@ export function expandNotifications({ maxId, forceLoad = false } = {}, done = no
         expandNotificationsController.abort();
         expandNotificationsController = new AbortController();
       } else {
-        done();
         return;
       }
     }
@@ -226,7 +225,8 @@ export function expandNotifications({ maxId, forceLoad = false } = {}, done = no
 
     dispatch(expandNotificationsRequest(isLoadingMore));
 
-    api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal }).then(response => {
+    try {
+      const response = await api().get('/api/v1/notifications', { params, signal: expandNotificationsController.signal });
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
       dispatch(importFetchedAccounts(response.data.map(item => item.account)));
@@ -236,11 +236,9 @@ export function expandNotifications({ maxId, forceLoad = false } = {}, done = no
       dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
       fetchRelatedRelationships(dispatch, response.data);
       dispatch(submitMarkers());
-    }).catch(error => {
+    } catch(error) {
       dispatch(expandNotificationsFail(error, isLoadingMore));
-    }).finally(() => {
-      done();
-    });
+    }
   };
 }
 
diff --git a/app/javascript/mastodon/actions/notifications_migration.tsx b/app/javascript/mastodon/actions/notifications_migration.tsx
index f856e5682..b453aed6e 100644
--- a/app/javascript/mastodon/actions/notifications_migration.tsx
+++ b/app/javascript/mastodon/actions/notifications_migration.tsx
@@ -13,6 +13,6 @@ export const initializeNotifications = createAppAsyncThunk(
     ) as boolean;
 
     if (enableBeta) void dispatch(fetchNotifications());
-    else dispatch(expandNotifications());
+    else void dispatch(expandNotifications());
   },
 );
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 44025bd59..630da3ef6 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -37,7 +37,7 @@ const randomUpTo = max =>
  * @param {string} channelName
  * @param {Object.<string, string>} params
  * @param {Object} options
- * @param {function(Function, Function): void} [options.fallback]
+ * @param {function(Function): Promise<void>} [options.fallback]
  * @param {function(): void} [options.fillGaps]
  * @param {function(object): boolean} [options.accept]
  * @returns {function(): void}
@@ -52,14 +52,13 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
     let pollingId;
 
     /**
-     * @param {function(Function, Function): void} fallback
+     * @param {function(Function): Promise<void>} fallback
      */
 
-    const useFallback = fallback => {
-      fallback(dispatch, () => {
-        // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
-        pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
-      });
+    const useFallback = async fallback => {
+      await fallback(dispatch);
+      // eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
+      pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
     };
 
     return {
@@ -132,21 +131,17 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
 
 /**
  * @param {Function} dispatch
- * @param {function(): void} done
  */
-const refreshHomeTimelineAndNotification = (dispatch, done) => {
-  // @ts-expect-error
-  dispatch(expandHomeTimeline({}, () =>
-    // @ts-expect-error
-    dispatch(expandNotifications({}, () =>
-      dispatch(fetchAnnouncements(done))))));
-};
+async function refreshHomeTimelineAndNotification(dispatch) {
+  await dispatch(expandHomeTimeline({ maxId: undefined }));
+  await dispatch(expandNotifications({}));
+  await dispatch(fetchAnnouncements());
+}
 
 /**
  * @returns {function(): void}
  */
 export const connectUserStream = () =>
-  // @ts-expect-error
   connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
 
 /**
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index f0ea46118..65b6d8045 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -76,21 +76,18 @@ export function clearTimeline(timeline) {
   };
 }
 
-const noOp = () => {};
-
 const parseTags = (tags = {}, mode) => {
   return (tags[mode] || []).map((tag) => {
     return tag.value;
   });
 };
 
-export function expandTimeline(timelineId, path, params = {}, done = noOp) {
-  return (dispatch, getState) => {
+export function expandTimeline(timelineId, path, params = {}) {
+  return async (dispatch, getState) => {
     const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
     const isLoadingMore = !!params.max_id;
 
     if (timeline.get('isLoading')) {
-      done();
       return;
     }
 
@@ -109,7 +106,8 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
 
     dispatch(expandTimelineRequest(timelineId, isLoadingMore));
 
-    api().get(path, { params }).then(response => {
+    try {
+      const response = await api().get(path, { params });
       const next = getLinks(response).refs.find(link => link.rel === 'next');
 
       dispatch(importFetchedStatuses(response.data));
@@ -127,52 +125,48 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
       if (timelineId === 'home') {
         dispatch(submitMarkers());
       }
-    }).catch(error => {
+    } catch(error) {
       dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
-    }).finally(() => {
-      done();
-    });
+    }
   };
 }
 
-export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) {
-  return (dispatch, getState) => {
+export function fillTimelineGaps(timelineId, path, params = {}) {
+  return async (dispatch, getState) => {
     const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
     const items = timeline.get('items');
     const nullIndexes = items.map((statusId, index) => statusId === null ? index : null);
     const gaps = nullIndexes.map(index => index > 0 ? items.get(index - 1) : null);
 
     // Only expand at most two gaps to avoid doing too many requests
-    done = gaps.take(2).reduce((done, maxId) => {
-      return (() => dispatch(expandTimeline(timelineId, path, { ...params, maxId }, done)));
-    }, done);
-
-    done();
+    for (const maxId of gaps.take(2)) {
+      await dispatch(expandTimeline(timelineId, path, { ...params, maxId }));
+    }
   };
 }
 
-export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
-export const expandPublicTimeline          = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
-export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
+export const expandHomeTimeline            = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
+export const expandPublicTimeline          = ({ maxId, onlyMedia, onlyRemote } = {}) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia });
+export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia });
 export const expandAccountTimeline         = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId });
 export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
 export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
-export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
-export const expandLinkTimeline            = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
-export const expandHashtagTimeline         = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
+export const expandListTimeline            = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
+export const expandLinkTimeline            = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId });
+export const expandHashtagTimeline         = (hashtag, { maxId, tags, local } = {}) => {
   return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
     max_id: maxId,
     any:    parseTags(tags, 'any'),
     all:    parseTags(tags, 'all'),
     none:   parseTags(tags, 'none'),
     local:  local,
-  }, done);
+  });
 };
 
-export const fillHomeTimelineGaps      = (done = noOp) => fillTimelineGaps('home', '/api/v1/timelines/home', {}, done);
-export const fillPublicTimelineGaps    = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done);
-export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
-export const fillListTimelineGaps      = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
+export const fillHomeTimelineGaps      = () => fillTimelineGaps('home', '/api/v1/timelines/home', {});
+export const fillPublicTimelineGaps    = ({ onlyMedia, onlyRemote } = {}) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia });
+export const fillCommunityTimelineGaps = ({ onlyMedia } = {}) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia });
+export const fillListTimelineGaps      = (id) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {});
 
 export function expandTimelineRequest(timeline, isLoadingMore) {
   return {
diff --git a/app/javascript/mastodon/features/link_timeline/index.tsx b/app/javascript/mastodon/features/link_timeline/index.tsx
index dd726dac1..262a21afc 100644
--- a/app/javascript/mastodon/features/link_timeline/index.tsx
+++ b/app/javascript/mastodon/features/link_timeline/index.tsx
@@ -36,13 +36,13 @@ export const LinkTimeline: React.FC<{
 
   const handleLoadMore = useCallback(
     (maxId: string) => {
-      dispatch(expandLinkTimeline(decodedUrl, { maxId }));
+      void dispatch(expandLinkTimeline(decodedUrl, { maxId }));
     },
     [dispatch, decodedUrl],
   );
 
   useEffect(() => {
-    dispatch(expandLinkTimeline(decodedUrl));
+    void dispatch(expandLinkTimeline(decodedUrl));
   }, [dispatch, decodedUrl]);
 
   return (