diff --git a/src/util/task-queue.js b/src/util/task-queue.js
index 7697104ae..06576dc84 100644
--- a/src/util/task-queue.js
+++ b/src/util/task-queue.js
@@ -1,5 +1,7 @@
 const Timer = require('../util/timer');
 
+const noop = () => {};
+
 /**
  * This class uses the token bucket algorithm to control a queue of tasks.
  */
@@ -48,10 +50,37 @@ class TaskQueue {
      *
      * @param {Function} task - the task to run.
      * @param {number} [cost=1] - the number of tokens this task consumes from the bucket.
-     * @returns {Promise} - a promise for the task's return value.
+     * @returns {Promise} - a promise for the task's return value, or `undefined` if the task gets canceled.
      * @memberof TaskQueue
      */
     do (task, cost = 1) {
+        return this._do(task, false, cost);
+    }
+
+    /**
+     * Wait until the token bucket is full enough, then run the provided task.
+     *
+     * @param {Function} task - the task to run.
+     * @param {number} [cost=1] - the number of tokens this task consumes from the bucket.
+     * @returns {Promise} - a promise for the task's return value. This promise will reject if the task gets canceled.
+     * @memberof TaskQueue
+     */
+    doOrReject (task, cost = 1) {
+        return this._do(task, true, cost);
+    }
+
+    /**
+     * Wait until the token bucket is full enough, then run the provided task.
+     *
+     * @param {Function} task - the task to run.
+     * @param {boolean} rejectOnCancel - choose what happens if the queue cancels this task:
+     *   true: reject the promise returned by this function
+     *   false: resolve the promise returned by this function with a value of `undefined`
+     * @param {number} [cost=1] - the number of tokens this task consumes from the bucket.
+     * @returns {Promise} - a promise for the task's return value.
+     * @memberof TaskQueue
+     */
+    _do (task, rejectOnCancel, cost) {
         if (this._maxTotalCost < Infinity) {
             const currentTotalCost = this._pendingTaskRecords.reduce((t, r) => t + r.cost, 0);
             if (currentTotalCost + cost > this._maxTotalCost) {
@@ -62,9 +91,13 @@ class TaskQueue {
             cost
         };
         newRecord.promise = new Promise((resolve, reject) => {
-            newRecord.cancel = () => {
-                reject(new Error('Task canceled'));
-            };
+            if (rejectOnCancel) {
+                newRecord.cancel = () => {
+                    reject(new Error('Task canceled'));
+                };
+            } else {
+                newRecord.cancel = noop;
+            }
 
             // The caller, `_runTasks()`, is responsible for cost-checking and spending tokens.
             newRecord.wrappedTask = () => {
diff --git a/test/unit/util_task-queue.js b/test/unit/util_task-queue.js
index 6e484a7a4..f26917317 100644
--- a/test/unit/util_task-queue.js
+++ b/test/unit/util_task-queue.js
@@ -76,13 +76,13 @@ test('run tasks', async t => {
     });
 });
 
-test('cancel', async t => {
+test('cancel with doOrReject', async t => {
     const bukkit = makeTestQueue();
 
     const taskResults = [];
     const goodCancelMessage = 'Task was canceled correctly';
     const afterCancelMessage = 'Task was run correctly';
-    const cancelTaskPromise = bukkit.do(
+    const cancelTaskPromise = bukkit.doOrReject(
         () => {
             taskResults.push('nope');
         }, 999);
@@ -94,7 +94,7 @@ test('cancel', async t => {
             taskResults.push(goodCancelMessage);
         }
     );
-    const keepTaskPromise = bukkit.do(
+    const keepTaskPromise = bukkit.doOrReject(
         () => {
             taskResults.push(afterCancelMessage);
             testCompare(t, bukkit._timer.timeElapsed(), '<', 10, 'Canceled task must not delay other tasks');
@@ -118,6 +118,39 @@ test('cancel', async t => {
     });
 });
 
+test('cancel with do (no reject)', async t => {
+    const bukkit = makeTestQueue();
+
+    const taskResults = [];
+    const afterCancelMessage = 'Task was run correctly';
+    const cancelTaskPromise = bukkit.do(
+        () => {
+            taskResults.push('nope');
+        }, 999);
+    const keepTaskPromise = bukkit.do(
+        () => {
+            taskResults.push(afterCancelMessage);
+            testCompare(t, bukkit._timer.timeElapsed(), '<', 10, 'Canceled task must not delay other tasks');
+        }, 5);
+
+    // give the bucket a chance to make a mistake
+    await bukkit._timer.advanceMockTimeAsync(1);
+
+    t.equal(bukkit.length, 2);
+    const taskWasCanceled = bukkit.cancel(cancelTaskPromise);
+    t.ok(taskWasCanceled);
+    t.equal(bukkit.length, 1);
+
+    while (bukkit.length > 0) {
+        await bukkit._timer.advanceMockTimeAsync(1);
+    }
+
+    return keepTaskPromise.then(() => {
+        t.deepEqual(taskResults, [afterCancelMessage]);
+        t.end();
+    });
+});
+
 test('cancelAll', async t => {
     const bukkit = makeTestQueue();
 
@@ -126,7 +159,7 @@ test('cancelAll', async t => {
     const goodCancelMessage2 = 'Task2 was canceled correctly';
 
     const promises = [
-        bukkit.do(() => taskResults.push('nope'), 999).then(
+        bukkit.doOrReject(() => taskResults.push('nope'), 999).then(
             () => {
                 t.fail('Task1 should have been canceled');
             },
@@ -134,7 +167,7 @@ test('cancelAll', async t => {
                 taskResults.push(goodCancelMessage1);
             }
         ),
-        bukkit.do(() => taskResults.push('nah'), 999).then(
+        bukkit.doOrReject(() => taskResults.push('nah'), 999).then(
             () => {
                 t.fail('Task2 should have been canceled');
             },