Delayed Logic & Scheduler

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 and the Jakarta Servlet 6 stack of the modular-webapp archetype, checked on 2026-10-08. Every timing and every output block below was produced with JDK 21, -sourceLevel 17 and Chrome 155 (headless) on the Bookshelf scenario of this section; the test outputs come from HtmlUnit and from a plain JVM. It is client knowledge: Timer and Scheduler are browser APIs compiled to JavaScript, so none of the code below can run on the server, and the shared module cannot use them (the build and JVM-test parts at the end are the exception). A browser runs your code on one thread, and everything on this page is about choosing when the next piece of your code gets that thread: later, repeatedly, after the current event, before the browser paints, or in small slices so the page stays responsive.

Choosing the right tool

GWT gives you two families of tools. com.google.gwt.user.client.Timer is a thin wrapper over the browser’s setTimeout and setInterval. com.google.gwt.core.client.Scheduler is a set of "low-level task scheduling primitives" (its own description) that cover everything else: running a command after the current event, around the GWT entry points, in time slices, and as fixed-delay or fixed-period loops.

Table 1. Which API for which job
You want to Use Notes

Run something once, N milliseconds from now, and be able to cancel it

Timer.schedule(int)

One object per pending job; cancel() is safe at any time

Repeat something at a fixed interval and be able to stop it from outside

Timer.scheduleRepeating(int)

Keeps firing even if run() throws

Repeat until the command itself says stop, with a constant gap after each run

Scheduler.scheduleFixedDelay

Return false from RepeatingCommand.execute() to stop; no handle to cancel from outside

Repeat until the command says stop, with a constant period

Scheduler.scheduleFixedPeriod

Runs can overlap in time if the work is asynchronous

Run once after the current event handler has finished

Scheduler.scheduleDeferred

Not a microtask: runs after native Promise callbacks

Run once, right before control returns to the browser

Scheduler.scheduleFinally

Batching small changes into one heavy operation

Run something every time the browser calls into GWT code

Scheduler.scheduleEntry

Instrumentation

Do a long computation or DOM update without freezing the page

Scheduler.scheduleIncremental

About 16 ms of work per slice, then the browser gets control back

Animate the user interface

AnimationScheduler (see Animations are not a job for Timer)

Both Timer and Scheduler Javadoc point there

flowchart TD start["Code that must run later"] --> many{"Repeats?"} many -- "no" --> when{"When?"} many -- "yes" --> who{"Who decides when to stop?"} when -- "after N ms, cancellable" --> t1["Timer.schedule"] when -- "after the current event" --> d["scheduleDeferred"] when -- "just before returning to the browser" --> f["scheduleFinally"] who -- "caller, from another place" --> t2["Timer.scheduleRepeating + cancel"] who -- "the command (return false)" --> kind{"Which pacing?"} kind -- "constant gap after each run" --> fd["scheduleFixedDelay"] kind -- "constant period" --> fp["scheduleFixedPeriod"] kind -- "as fast as the page allows, in slices" --> inc["scheduleIncremental"]

The event loop in one minute

A browser executes JavaScript in tasks. The page loads, a click arrives, a timer fires, an XMLHttpRequest finishes: each is a task, and while one task runs nothing else happens on the page, including painting and further input. When the task returns, the browser runs the pending microtasks (native Promise callbacks), may paint, and then picks the next task. Two consequences drive the whole page.

  • Code that does not return control to the browser freezes the page. The Developer Guide says a script that runs for more than about 10 seconds risks the browser’s unresponsive-script dialog.

  • "Later" always means "in a later task". Even schedule(0) cannot run until the current task has returned.

GWT-compiled code wraps every call that comes from outside (event handlers, timer callbacks, XHR callbacks, the module load) in an internal entry function, the $entry that you also meet in JSNI & Overlay Types. That wrapper is where the Scheduler hooks in and where uncaught exceptions are routed to GWT.UncaughtExceptionHandler.

sequenceDiagram participant B as Browser event loop participant G as GWT entry wrapper participant H as Your handler participant Q as Scheduler queues B->>G: task: click event G->>Q: run entry commands G->>H: onClick H->>Q: scheduleFinally, scheduleDeferred, scheduleIncremental H->>B: Timer.schedule(0), scheduleFixedDelay(0) H-->>G: returns G->>Q: run finally commands G-->>B: task ends B->>B: microtasks (native Promise callbacks) B->>G: task: timer callbacks (setTimeout 0) B->>G: task: scheduler flusher (setTimeout 1 ms) G->>Q: deferred commands, then incremental slice

Timer

Timer is an abstract class: subclass it, override run(), and call schedule or scheduleRepeating. The Javadoc says it serves the same purpose as java.util.Timer, simplified for a single-threaded environment.

Table 2. The whole Timer API
Member Behaviour (checked in the 2.13.1 source and by running it)

abstract void run()

Called when the timer fires. It runs inside the GWT entry wrapper, so an uncaught exception reaches the uncaught-exception handler

void schedule(int delayMillis)

One-shot. Throws IllegalArgumentException("must be non-negative") for a negative delay. If the timer is already running it is cancelled first, so a timer never has two pending runs

void scheduleRepeating(int periodMillis)

Repeats until cancelled. Throws IllegalArgumentException("must be positive") for zero or a negative period. A running timer is cancelled first, which also lets you switch a timer between one-shot and repeating mode

void cancel()

Stops the timer. A no-op if the timer is not running, so calling it twice is fine

final boolean isRunning()

true from the moment of scheduling until the one-shot run starts or cancel() is called. For a repeating timer it stays true until cancelled

Schedule, reschedule, repeat and cancel

A one-shot timer, a reschedule, argument checks, a repeating timer that cancels itself
Timer t = new Timer() {
  @Override
  public void run() {
    log("t.run() fired, isRunning=" + isRunning());
  }
};
t.schedule(100);
t.schedule(200);                       // cancels the first schedule: only one run happens
try { t.schedule(-1); } catch (IllegalArgumentException e) { log(e.getMessage()); }
try { t.scheduleRepeating(0); } catch (IllegalArgumentException e) { log(e.getMessage()); }

Timer repeating = new Timer() {
  int n;

  @Override
  public void run() {
    n++;
    log("repeating run #" + n);
    if (n == 3) {
      cancel();                        // a timer may cancel itself from run()
    }
  }
};
repeating.scheduleRepeating(100);

Output (milliseconds since the module loaded; the rescheduled one-shot fires once, 200 ms after the second call, and the two failed calls leave the pending run untouched):

     1 ms  new Timer: isRunning=false
     1 ms  after schedule(100): isRunning=true
     1 ms  schedule(-1) -> IllegalArgumentException: must be non-negative
     1 ms  scheduleRepeating(0) -> IllegalArgumentException: must be positive
     1 ms  scheduleRepeating(-5) -> IllegalArgumentException: must be positive
     1 ms  after the failed calls: isRunning=true (the earlier schedule(200) is still pending)
     1 ms  cancelled twice without error; isRunning=false
   201 ms  t.run() fired, isRunning=false
   506 ms  repeating run #1
   604 ms  repeating run #2
   704 ms  repeating run #3
   704 ms  cancel() inside run(): isRunning=false

A timer that was scheduled with schedule(700) and then with scheduleRepeating(900) fired only the repeating variant, first after 900 ms, then every 900 ms: the second call replaced the first and changed the mode.

A timeout around a remote call

The Developer Guide’s classic use of Timer is a timeout around a long operation. Its rules of thumb still hold: keep the timer in a field, never start a second one while one is pending, cancel it when the work succeeds, and clear the field when the work ends or the timer expires. Here is the pattern on the Bookshelf catalogue service (the asynchronous interface from GWT-RPC), with a 200 ms limit:

A timeout around CatalogServiceAsync.getBooks
private Timer timeoutTimer;
private boolean aborted;

private void load(CatalogServiceAsync service, int limitMs) {
  if (timeoutTimer != null) {
    log("already running");
    return;
  }
  aborted = false;
  timeoutTimer = new Timer() {
    @Override
    public void run() {
      timeoutTimer = null;
      aborted = true;
      log("timeout expired -> abort flag set");
    }
  };
  timeoutTimer.schedule(limitMs);

  service.getBooks(0, 25, new AsyncCallback<BookPage>() {
    @Override
    public void onSuccess(BookPage page) {
      cancelTimeout();
      if (aborted) {
        log("late answer discarded");
        return;
      }
      show(page);
    }

    @Override
    public void onFailure(Throwable caught) {
      cancelTimeout();
    }
  });
}

private void cancelTimeout() {
  if (timeoutTimer != null) {
    timeoutTimer.cancel();
    timeoutTimer = null;
  }
}

Against a stub service whose latency was 500 ms, then 100 ms (a second load call while the first is pending is refused):

     0 ms  RPC timeout pattern with a 200 ms limit; service latency 500 ms
     0 ms    request sent (in flight: 1)
     0 ms  already running
   201 ms  timeout expired -> abort flag set
   501 ms  late answer discarded
   902 ms  --- second run: latency 100 ms (answers before the timeout)
   902 ms    request sent (in flight: 1)
  1002 ms  answer in time: total=25

The timer does not cancel the request: the browser still receives the answer at 501 ms, which is why the callback checks the flag. A timeout like this only stops the user interface from waiting. If the server work must also stop, the server needs its own deadline.

Timers belong to the widget that starts them

A repeating Timer keeps a reference to your object and fires until somebody cancels it, including after the widget that created it has left the page. The reliable place to start and stop it is the widget lifecycle: start in onLoad() (called when the widget is attached) and cancel in onUnload() (called when it is detached). In an Activity, the equivalent is start(...) and onStop()/onCancel() (see MVP, Activities & Places).

A label that updates itself only while it is attached
class LastUpdatedLabel extends Label {
  private int ticks;
  private final Timer ticker = new Timer() {
    @Override
    public void run() {
      setText("tick " + (++ticks));
    }
  };

  @Override
  protected void onLoad() {
    super.onLoad();
    ticker.scheduleRepeating(100);
  }

  @Override
  protected void onUnload() {
    ticker.cancel();
    super.onUnload();
  }
}

Added to the RootPanel and removed after 450 ms the label showed tick 4, the timer reported isRunning=true just before onUnload() cancelled it, and 500 ms later the text was still tick 4.

How exact are timers?

They are not. A timer fires no earlier than requested and, if the page is busy, later. The MDN reference for setTimeout lists the reasons: nested timers get a minimum delay of about 4 ms once they have been chained several times, and browsers throttle timers in inactive tabs (Firefox desktop to one second, Chrome with several throttling levels). The throttling showed up while preparing this page: with the page in a hidden browser pane, scheduleDeferred and Timer.schedule(300) ran about 720 ms after they were requested. The same code in a visible page ran on time. Do not use a timer to measure time; compare Duration.currentTimeMillis() (or System.currentTimeMillis()) values instead, as the Developer Guide’s elapsed-time example does.

Animations are not a job for Timer

Both Javadocs carry the same note: if you are using a timer to schedule a user interface animation, use com.google.gwt.animation.client.AnimationScheduler instead, because the browser can then optimise the animation (it wraps requestAnimationFrame). Use Timer and Scheduler for logic, not for frames.

Scheduler

Scheduler.get() returns the one default implementation. Scheduler has two command interfaces and eight scheduling methods (six names, two of them overloaded). Any exception thrown by a command is passed to the installed GWT.UncaughtExceptionHandler, if there is one.

Table 3. The command interfaces
Interface Method Purpose

Scheduler.ScheduledCommand

void execute()

A command that runs once. com.google.gwt.user.client.Command extends it ("to help migrate from DeferredCommand"), so existing Command objects can be passed in

Scheduler.RepeatingCommand

boolean execute()

Return true to be invoked again, false to stop

Table 4. Every Scheduler method
Method Runs Notes from the Javadoc and the source

scheduleDeferred(ScheduledCommand)

Once, "after the browser event loop returns"

Queued, then flushed by an internal timer of 1 ms

scheduleEntry(ScheduledCommand) and scheduleEntry(RepeatingCommand)

Before GWT-generated code is invoked by the browser’s event loop

The repeating form is called once per entry until it returns false. An entry command that schedules another entry command runs before control reaches the GWT code

scheduleFinally(ScheduledCommand) and scheduleFinally(RepeatingCommand)

Before GWT-generated code returns control to the event loop

The repeating form is called once per exit until it returns false. A finally command that schedules another finally command runs before control returns to the browser. Good for aggregating small amounts of work before one heavy operation

scheduleFixedDelay(RepeatingCommand, int delayMs)

After delayMs, then again delayMs after the previous run ends

Constant delay between the end of one run and the start of the next

scheduleFixedPeriod(RepeatingCommand, int delayMs)

Every delayMs

Constant period, regardless of how long the previous run took

scheduleIncremental(RepeatingCommand)

Many times in rapid succession, then yields

For long-running computation or DOM manipulation; the browser processes its queue between bursts

Nothing in this API returns a handle. To cancel a Scheduler loop from outside, keep a flag that the command reads and return false when it is set; to cancel by handle, use Timer.

scheduleDeferred: after the current event

The Developer Guide’s use case is initialisation order: the code that must wait for the rest of the current event to finish. A deferred command runs only after the handler that scheduled it has returned, so objects created later in the same method already exist:

private TextBox dataEntry;

private void build() {
  // Set the focus on the widget after the setup below has completed.
  Scheduler.get().scheduleDeferred(() -> dataEntry.setFocus(true));

  dataEntry = new TextBox();
  RootPanel.get().add(dataEntry);
}

The book uses the same idea after a window resize: it defers the repositioning of a slider until the browser has finished resizing (see Outdated material in older books and posts).

The measured order

The documentation says in words what runs "before" and "after". The following program schedules one of each kind twice: from onModuleLoad and from a click handler that a timer triggers 300 ms later. A native Promise.resolve().then(...) wrapped in $entry is included for comparison.

Scheduling every kind of command from one place
private void schedule(String p) {
  Scheduler.get().scheduleDeferred(() -> log(p + "scheduleDeferred"));
  Scheduler.get().scheduleEntry(() -> log(p + "scheduleEntry"));
  Scheduler.get().scheduleFinally(() -> {
    log(p + "scheduleFinally");
    Scheduler.get().scheduleFinally(() -> log(p + "  nested scheduleFinally"));
  });
  new Timer() {
    @Override
    public void run() { log(p + "Timer.schedule(0)"); }
  }.schedule(0);
  Scheduler.get().scheduleFixedDelay(() -> { log(p + "scheduleFixedDelay(0)"); return false; }, 0);
  Scheduler.get().scheduleFixedPeriod(() -> { log(p + "scheduleFixedPeriod(0)"); return false; }, 0);
  Scheduler.get().scheduleIncremental(() -> { log(p + "scheduleIncremental"); return false; });
  microtask(() -> log(p + "Promise microtask (native, for comparison)"));
}

The same order came out of three separate runs, from the click handler and from onModuleLoad:

     0 ms  onModuleLoad: start
     0 ms  onModuleLoad: end
     0 ms  [load] scheduleFinally
     0 ms  [load]   nested scheduleFinally (scheduled from a finally command)
     0 ms  [load] scheduleEntry
     0 ms  [load] Promise microtask (native, for comparison)
     0 ms  [load] Timer.schedule(0)
     0 ms  [load] scheduleFixedDelay(0)
     0 ms  [load] scheduleFixedPeriod(0)
     2 ms  [load] scheduleDeferred
     2 ms  [load] scheduleIncremental
   303 ms  click handler: start
   303 ms  click handler: end
   303 ms  [click] scheduleFinally
   303 ms  [click]   nested scheduleFinally (scheduled from a finally command)
   303 ms  [click] scheduleEntry
   303 ms  [click] Promise microtask (native, for comparison)
   306 ms  [click] Timer.schedule(0)
   306 ms  [click] scheduleFixedDelay(0)
   306 ms  [click] scheduleFixedPeriod(0)
   306 ms  [click] scheduleDeferred
   306 ms  [click] scheduleIncremental

Reading the log:

  1. scheduleFinally ran as soon as the handler returned, and the finally command it scheduled itself ran before control returned to the browser.

  2. scheduleEntry did not run at the end of the handler. It ran the next time the browser entered GWT code, which here was the Promise callback (wrapped in $entry). An entry command is a hook for "something is about to happen", not a way to run code soon.

  3. The native Promise callback ran before any timer-based command. A deferred command is not a microtask: it is flushed by an internal 1 ms timer, so it runs after Timer.schedule(0), scheduleFixedDelay(0) and scheduleFixedPeriod(0) that were requested in the same task.

  4. scheduleIncremental shares the deferred queue, so its first run came right after the deferred command.

Do not build logic on the relative order of timer-based commands scheduled in the same millisecond. Rely only on what the Javadoc promises: finally and entry commands bracket the GWT code, deferred commands wait for the event loop.

scheduleFixedDelay and scheduleFixedPeriod

Both take a RepeatingCommand and a number of milliseconds. They differ in what the time is measured from. A command that takes 30 ms with a 100 ms parameter, five runs each:

The same 30 ms command under both pacing modes
Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
  int n;

  @Override
  public boolean execute() {
    log("fixedDelay(100) start #" + n);
    busy(30);                            // a 30 ms busy loop stands in for real work
    return ++n < 5;
  }
}, 100);
// the same body with Scheduler.get().scheduleFixedPeriod(..., 100)
   100 ms  fixedDelay(100) start #0
   233 ms  fixedDelay(100) start #1  (+133 since previous start)
   365 ms  fixedDelay(100) start #2  (+132 since previous start)
   497 ms  fixedDelay(100) start #3  (+132 since previous start)
   629 ms  fixedDelay(100) start #4  (+132 since previous start)
  1104 ms  fixedPeriod(100) start #0
  1206 ms  fixedPeriod(100) start #1  (+102 since previous start)
  1305 ms  fixedPeriod(100) start #2  (+99 since previous start)
  1405 ms  fixedPeriod(100) start #3  (+100 since previous start)
  1505 ms  fixedPeriod(100) start #4  (+100 since previous start)

Fixed delay measures from the end of the previous run (30 ms of work plus the 100 ms delay, about 132 ms between starts), which is what you want when work must never pile up. Fixed period measures from the start (100 ms between starts), which suits a heartbeat but, as the polling section shows, lets asynchronous work overlap. The first run of scheduleFixedDelay also waits one full delay.

scheduleIncremental: work in slices

scheduleIncremental accepts a RepeatingCommand that does a small amount of work per execute() and returns true while more remains. The 2.13.1 implementation keeps incremental commands in one queue and runs them round-robin for up to 16 ms (the source says this "allows control to be returned to the browser 60 times a second"), then yields with a 1 ms timer so the browser can process input and paint. Another timer of 50 ms acts as a safety net if the flusher is killed, for example by a slow-script warning.

A 600 x 1 ms computation, first as one synchronous loop and then as 600 incremental calls, while a repeating Timer of 10 ms measured how long the page was unresponsive:

   202 ms  synchronous loop of 600 x 1 ms starts
   802 ms  synchronous loop ends
   903 ms    heartbeat (10 ms) max gap during the loop: 601 ms
   903 ms  scheduleIncremental: 600 x 1 ms units, one unit per execute()
  1676 ms  scheduleIncremental finished: 600 execute() calls
  1676 ms    calls per slice for the first slices: [65 calls, 16 calls, 16 calls, 16 calls, 16 calls, 16 calls]
  1676 ms    heartbeat (10 ms) max gap during the run: 21 ms

The synchronous loop made the timer miss 600 ms; the incremental version never made it wait more than 21 ms, at the price of a longer total time (773 ms against 600 ms). The first slice held 65 calls in this run, and the later slices settled at 16 calls of 1 ms each. A call must therefore be small: a single execute() that itself runs for a second cannot be sliced by the scheduler.

What happens when a command throws

The Javadoc says exceptions go to the uncaught-exception handler. What each API does afterwards differs, and it matters for polling loops. The program installed GWT.setUncaughtExceptionHandler(...) and made each kind of command throw IllegalStateException on its second run:

Table 5. What an exception does to each kind of scheduled work
Work Observed after the second run threw Consequence

scheduleDeferred

The handler was called; the next deferred command still ran

One failure does not poison the queue

Timer.schedule

The handler was called

One-shot, nothing to continue

Timer.scheduleRepeating

The handler was called and the timer kept firing (runs 3 and 4 followed)

A repeating timer never stops by itself

scheduleFixedDelay

The handler was called and there was no third run

The loop ends silently apart from the handler

scheduleFixedPeriod

The handler was called and there was no third run

Same

scheduleIncremental

The handler was called; the command stayed queued and ran again about 50 ms later (runs 3 to 5)

A command that always throws is retried over and over

Catch exceptions inside the command and return the value you mean, rather than relying on these side effects. In a polling loop, a failure should be a deliberate decision to stop, retry or back off.

Breaking up long-running work

The Developer Guide motivates scheduleIncremental with the browser’s unresponsive-script dialog. The practical rule is simpler: any task that keeps the thread for a noticeable fraction of a second makes the page feel stuck, and a few seconds can bring up the browser’s warning. Typical Bookshelf culprits are building a search index for the whole catalogue, rendering a very long list into the DOM, or parsing a large JSON document.

The recipe has four parts: keep the state in fields, do a bounded chunk per execute(), report progress, and let the caller cancel.

A cancellable batch job
class BatchedIndexer {
  interface Listener {
    void onProgress(int done, int total);

    void onDone(int distinctTitles);
  }

  private final List<Book> books;
  private final Listener listener;
  private final int chunk;
  private final Set<String> titles = new HashSet<>();
  private int next;
  private boolean cancelled;

  BatchedIndexer(List<Book> books, int chunk, Listener listener) {
    this.books = books;
    this.chunk = chunk;
    this.listener = listener;
  }

  void start() {
    Scheduler.get().scheduleIncremental(() -> {
      if (cancelled) {
        return false;
      }
      int end = Math.min(next + chunk, books.size());
      for (; next < end; next++) {
        titles.add(slug(books.get(next).getTitle() + " " + books.get(next).getAuthor()));
      }
      listener.onProgress(next, books.size());
      if (next < books.size()) {
        return true;                       // more to do: the scheduler calls us again
      }
      listener.onDone(titles.size());
      return false;                        // finished: leave the queue
    });
  }

  void cancel() {
    cancelled = true;
  }

  static String slug(String s) {
    return s.toLowerCase().replaceAll("[^a-z0-9]+", "-").replaceAll("-+$", "");
  }
}

Run over 200 000 synthetic Book objects with chunks of 500, next to a single loop over the same data (a 10 ms heartbeat Timer measures responsiveness; the regular expressions make each item costly enough to matter):

   270 ms  one synchronous loop over 200000 books: 58 ms, distinct=15000
   371 ms    heartbeat max gap: 59 ms
   391 ms    progress 50000/200000
   404 ms    progress 100000/200000
   418 ms    progress 150000/200000
   432 ms    progress 200000/200000
   432 ms  BatchedIndexer(chunk=500) done: 400 chunks, 61 ms, distinct=15000
   432 ms    heartbeat max gap: 18 ms

Total time is practically the same; what changes is that the page was blocked for 59 ms in one case and at most 18 ms in the other. Notes on the design:

  • Pick the chunk so that one call is a fraction of a millisecond to a few milliseconds. The scheduler itself decides how many calls fit into a slice, so a smaller chunk only costs a little more overhead.

  • Do not touch the DOM in every chunk if you can avoid it: collect, then update once (or in pages) so the browser lays out less often. scheduleFinally is the tool for "collect during this event, apply once at the end".

  • If the work is truly heavy and independent of the DOM, a Web Worker is the next step, reached through JsInterop (see JsInterop and DOM & Elemental2). Slicing keeps one thread responsive; it does not add CPU.

Polling the server

Polling is a Timer or Scheduler loop that calls the server. It looks trivial, and it is where the differences between the pacing modes show. The two requirements are that requests must not overlap and that failures must back off.

First, the naive version: scheduleFixedPeriod(..., 100) sending a request each time, against a service that takes 250 ms to answer:

     0 ms  A) fixed period 100 ms, service latency 250 ms (requests overlap)
   101 ms    request sent (in flight: 1)
   201 ms    request sent (in flight: 2)
   301 ms    request sent (in flight: 3)
   401 ms    request sent (in flight: 3)
   502 ms    request sent (in flight: 3)
   602 ms    request sent (in flight: 3)
  1501 ms  A) max requests in flight: 3

Requests pile up as soon as the server is slower than the period, which is exactly when it can least afford them. The robust shape schedules the next poll from the callback of the previous one:

A poller that never has two requests in flight
class CatalogPoller {
  interface Listener {
    void onTotal(int total);

    void onError(Throwable t);
  }

  private final CatalogServiceAsync service;
  private final Listener listener;
  private final int intervalMs;
  private final Timer timer = new Timer() {
    @Override
    public void run() {
      poll();
    }
  };
  private boolean running;

  CatalogPoller(CatalogServiceAsync service, int intervalMs, Listener listener) {
    this.service = service;
    this.intervalMs = intervalMs;
    this.listener = listener;
  }

  void start() {
    if (!running) {
      running = true;
      poll();
    }
  }

  void stop() {
    running = false;
    timer.cancel();
  }

  private void poll() {
    service.getBooks(0, 1, new AsyncCallback<BookPage>() {
      @Override
      public void onSuccess(BookPage page) {
        if (!running) {
          return;                            // stopped while the request was in flight
        }
        listener.onTotal(page.getTotal());
        timer.schedule(intervalMs);
      }

      @Override
      public void onFailure(Throwable t) {
        if (!running) {
          return;
        }
        listener.onError(t);
        timer.schedule(intervalMs * 4);      // back off after a failure
      }
    });
  }
}

Same 250 ms service, 100 ms interval:

  1501 ms  B) CatalogPoller: next poll scheduled from the callback, 100 ms after the answer, latency 250 ms
  1501 ms    request sent (in flight: 1)
  1753 ms    total=25
  1855 ms    request sent (in flight: 1)
  2105 ms    total=25
  2206 ms    request sent (in flight: 1)
  2458 ms    total=25
  2559 ms    request sent (in flight: 1)
  2703 ms  B) stopped; max requests in flight: 1

Things the poller gets right, and things to add in a real application:

  • It only schedules the next poll in the callback, so the effective cycle is "latency + interval" and the server load adapts to its own speed.

  • stop() also covers the answer that arrives after stopping (running is checked), the equivalent of the late answer in the timeout example.

  • Tie start() and stop() to a lifecycle: onLoad/onUnload of a widget, or start/onStop of an Activity, as in the widget example above. A poller nobody stops keeps loading the server for a page nobody is looking at.

  • A real service call needs an XSRF token and is subject to the same-origin rules, so use the services of GWT-RPC and XSRF, RPC & Server Security unchanged; polling does not exempt a call from them. Authorisation stays on the server.

  • Choose the interval from the data’s freshness requirement, add some random jitter if thousands of browsers start at once, and stop polling when the tab is hidden if staleness is acceptable (the standard Page Visibility API, reached through DOM & Elemental2).

  • When updates must arrive within seconds, polling is wasteful. Server push through WebSockets or Server-Sent Events (reached with Elemental2, see DOM & Elemental2; REST & JSON Libraries lists the alternatives) is the next step, and RequestBuilder, JSON & XML covers the plain HTTP route.

Testing delayed code

Delayed code is hard to test because the test method returns before the code runs. Two techniques cover it. The general test setup is in Testing.

In a GWT test (HtmlUnit). GWTTestCase runs the test method, and the test ends when the method returns, unless it calls delayTestFinish(timeoutMillis). The test then lasts until finishTest() is called or the timeout expires.

A GWTTestCase for Timer and Scheduler
public class DelayedLogicTest extends GWTTestCase {
  @Override
  public String getModuleName() {
    return "com.example.bookshelf.TimingTest";
  }

  public void testDeferredRunsAfterTheTestMethodReturns() {
    final List<String> order = new ArrayList<>();
    delayTestFinish(2000);
    Scheduler.get().scheduleDeferred(() -> {
      order.add("deferred");
      assertEquals("[sync, deferred]", order.toString());
      finishTest();
    });
    order.add("sync");
  }

  public void testTimerFires() {
    delayTestFinish(2000);
    new Timer() {
      @Override
      public void run() {
        finishTest();
      }
    }.schedule(100);
  }

  public void testIncrementalRunsUntilFalse() {
    final int[] n = {0};
    delayTestFinish(2000);
    Scheduler.get().scheduleIncremental(() -> {
      if (++n[0] < 50) {
        return true;
      }
      assertEquals(50, n[0]);
      finishTest();
      return false;
    });
  }
}

All three passed. A fourth test that called delayTestFinish(300) and never reached finishTest() failed with:

com.google.gwt.junit.client.TimeoutException: A timeout expired after 300ms elapsed.

In a plain JVM test. GWT test cases are slow, so the logic around the scheduler is better tested on the JVM. That requires not calling Scheduler.get() from the class: inject the Scheduler through the constructor and use com.google.gwt.core.client.testing.StubScheduler in the test. It records commands and runs them when you ask.

A class that receives its Scheduler
public class CatalogRefresher {
  private final Scheduler scheduler;
  private final Runnable refresh;
  private int remaining;

  public CatalogRefresher(Scheduler scheduler, Runnable refresh) {
    this.scheduler = scheduler;
    this.refresh = refresh;
  }

  public void start(int times, int delayMs) {
    remaining = times;
    scheduler.scheduleFixedDelay(() -> {
      refresh.run();
      return --remaining > 0;
    }, delayMs);
  }
}
The JVM test with StubScheduler
@Test
public void refreshesThreeTimesThenStops() {
  StubScheduler scheduler = new StubScheduler();
  int[] calls = {0};
  new CatalogRefresher(scheduler, () -> calls[0]++).start(3, 30_000);

  assertEquals(1, scheduler.getRepeatingCommands().size());
  assertTrue(scheduler.executeRepeatingCommands());   // refresh 1, still scheduled
  assertTrue(scheduler.executeRepeatingCommands());   // refresh 2
  assertFalse(scheduler.executeRepeatingCommands());  // refresh 3: the command returned false
  assertEquals(3, calls[0]);
  assertEquals(0, scheduler.getRepeatingCommands().size());
}

The test ran in 10 ms on the JVM. StubScheduler also has executeCommands() and executeScheduledCommands() for the one-shot commands. Timer has no such stub: it is native code, so keep Timer in a thin class (or wrap it behind an interface) and put the logic in classes you can test with StubScheduler. Calling Scheduler.get() directly on the JVM fails in the class initialiser:

java.lang.ExceptionInInitializerError caused by java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code!  It cannot be called, for example, from server code.  If you are running a unit test, check that your test case extends GWTTestCase and that GWT.create() is not called from within an initializer or constructor.

DeferredCommand and IncrementalCommand (deprecated)

Before Scheduler existed, GWT offered two static helpers, and both are still in gwt-user 2.13.1, marked @Deprecated. Code from older tutorials and from books compiles, and you will meet it in legacy projects.

Table 6. Old API and its replacement
Deprecated Replacement Why (from the Javadoc)

DeferredCommand.addCommand(Command)

Scheduler.get().scheduleDeferred(...)

"the static nature of this API prevents effective mocking for JRE-only tests"

DeferredCommand.add(Command)

scheduleDeferred

Already deprecated "as of release 1.4" in favour of addCommand

DeferredCommand.addCommand(IncrementalCommand)

Scheduler.get().scheduleIncremental(...)

Same

DeferredCommand.addPause()

None

"with no replacement because the presence of this method causes arbitrary scheduling decisions"

IncrementalCommand

Scheduler.RepeatingCommand

IncrementalCommand now extends RepeatingCommand, so an old command can be passed to scheduleIncremental unchanged

The old class is not just a different spelling of the new one. This program mixes both in one handler:

     1 ms  start
     1 ms  end
     2 ms  Scheduler.scheduleDeferred #1
     2 ms  Scheduler.scheduleDeferred #2
     2 ms  IncrementalCommand passed to Scheduler.scheduleIncremental #1
     2 ms  IncrementalCommand passed to Scheduler.scheduleIncremental #2
     2 ms  IncrementalCommand passed to Scheduler.scheduleIncremental #3
     2 ms  DeferredCommand.addCommand(Command)
     6 ms  DeferredCommand after addPause()
     6 ms  DeferredCommand.addCommand(IncrementalCommand) #1
     6 ms  DeferredCommand.addCommand(IncrementalCommand) #2
     6 ms  DeferredCommand.addCommand(IncrementalCommand) #3

DeferredCommand keeps its own queue, so its commands did not interleave with the Scheduler ones even though DeferredCommand.addCommand(Command) was called between two scheduleDeferred calls; and addPause() pushed the following commands to a later turn of the event loop. Migrating therefore means moving all the related deferred code to Scheduler together; mixing the two APIs gives no ordering guarantee. Because Command extends ScheduledCommand, the migration of a call is mechanical:

// before
DeferredCommand.addCommand(new Command() {
  @Override
  public void execute() {
    redimensionalise();
  }
});

// after
Scheduler.get().scheduleDeferred(this::redimensionalise);

Pitfalls

Table 7. Common problems with delayed logic
Symptom Cause Fix

java.lang.IllegalArgumentException: must be non-negative from Timer.schedule

A negative delay, often a computed value (deadline - now)

Clamp with Math.max(0, delay)

java.lang.IllegalArgumentException: must be positive from Timer.scheduleRepeating

A period of 0 or less

Use at least 1 ms; for "as fast as possible" use scheduleIncremental

A second schedule call "loses" the first run

One Timer object has one pending run; scheduling again cancels the previous

Use one Timer per independent job

A timer keeps running after its widget or Activity is gone

Nobody called cancel()

Start in onLoad, cancel in onUnload; in an Activity, cancel in onStop/onCancel

A repeating Timer keeps firing after run() threw

Timer.scheduleRepeating continues after an exception, unlike scheduleFixedDelay and scheduleFixedPeriod

Catch inside run() and call cancel() on purpose

An incremental command throws forever

After an exception it stays in the queue and is retried about 50 ms later

Catch exceptions inside the command and return false

Polling requests pile up, the server gets slower

scheduleFixedPeriod or scheduleRepeating does not wait for the asynchronous answer

Schedule the next poll from the callback, as in CatalogPoller

The page freezes in one event handler

The handler runs a long loop on the only thread

Slice it with scheduleIncremental; the Developer Guide mentions the browser’s slow-script dialog after about 10 seconds

scheduleIncremental still freezes the page

A single execute() does too much work, and the scheduler cannot split it

Smaller chunks per call

scheduleDeferred runs after a native Promise callback

A deferred command is a timer-based task, not a microtask

Do not mix the two when order matters; use one mechanism

Timers run late in a background tab

Browsers throttle inactive tabs (measured here: about 720 ms of extra delay in a hidden pane)

Do not rely on timer precision; pause polling in hidden tabs

The method format(String, long, String) is undefined for the type String in a timing log helper

String.format is not part of the JRE emulation

Concatenate strings, or use NumberFormat/DateTimeFormat

ExceptionInInitializerError caused by ERROR: GWT.create() is only usable in client code! in a JVM test

The class under test calls Scheduler.get(), whose class initialiser calls GWT.create

Inject the Scheduler and use StubScheduler

com.google.gwt.junit.client.TimeoutException: A timeout expired after 300ms elapsed.

delayTestFinish was called but finishTest() never ran

Call finishTest() from the last callback, or raise the timeout

A test passes although its asynchronous assertion never ran

No delayTestFinish: the test ends when the method returns

Always pair asynchronous work with delayTestFinish and finishTest

Old code that mixes DeferredCommand and Scheduler runs in a surprising order

Two separate queues

Migrate related code together to Scheduler

Outdated material in older books and posts

The two chapters of GWT in Action (written for GWT 1.3 and 1.4) that touch this topic predate Scheduler: the file Scheduler.java in the 2.13.1 sources carries a 2009 copyright header, two years after the book. The book therefore uses Timer (for example to switch a button’s style back after a click, and for the update timer of its Server Status component) and DeferredCommand (to resize a slider after the browser has finished a window resize). Read them for the reasoning, and translate the code:

  • DeferredCommand.addCommand(new Command() {...}) becomes Scheduler.get().scheduleDeferred(...), and IncrementalCommand becomes RepeatingCommand/scheduleIncremental.

  • ClickListener, WindowResizeListener and Composite-based "updateable" components are the pre-handler event model; use handlers and a widget with onLoad/onUnload for the timer, as shown above.

  • The book’s polling chapter also describes emulating server push by blocking server threads. With today’s servlet containers that holds a thread per client; prefer WebSockets or Server-Sent Events, and treat polling as the fallback.

  • The Developer Guide page itself is partly stale: its table of contents still announces DeferredCommand and IncrementalCommand, while the examples further down use Scheduler, and the IncrementalCommand example in the page is only an outline (it uses a result variable that it never declares). The Javadoc and the source are the reference.

Where a statement on this page says "measured", it is from the run described in the first paragraph, not from the Developer Guide.

What changed recently

The release notes are short on these classes. They mention a performance fix in RepeatingCommand (2.5.1 RC1, issue 7307), a Timer.cancel() fix for old Internet Explorer versions (2.6.0 RC1, issue 8101; the cancelCounter workaround is still in the 2.13.1 Timer source), and the webApiUsage property (2.5.1 RC1), whose stable setting makes the animation library use a timer instead of requestAnimationFrame. Nothing newer concerns Timer or Scheduler. DeferredCommand and IncrementalCommand are deprecated but not removed in 2.13.1. The exact numbers on this page (the 16 ms slice, the 1 ms and 50 ms internal timers) are implementation details of SchedulerImpl in 2.13.1, not documented guarantees.