Modernizing Protractor: Disable Control Flow and Adopt async/await
Protractor's legacy Control Flow is deprecated. Disable it with controlFlow: null in your config, refactor specs to use async/await, and follow a verification checklist to modernize your test suite while keeping Angular sync intact.
17 May 2026, 02:04 UTC

Why Protractor’s Control Flow is a Problem
Protractor originally used Selenium’s Control Flow to queue commands. The Flow treated every command as if it were synchronous, hiding the underlying promises. While convenient, it made test code opaque and brittle. The Flow has been deprecated and removed in the latest Protractor releases, so keeping it enabled forces you to write legacy code that no longer works with modern JavaScript patterns.
How to Turn it Off
Disabling the Flow turns every Protractor command into a real Promise. That means you can use async/await naturally and the test runner will no longer try to “magically” order your actions.
// protractor.conf.js
exports.config = {
// ... other settings
controlFlow: null, // disable the legacy Control Flow
};
After adding controlFlow: null, run protractor and you should see no warnings about the Control Flow. In a test file you can verify it by printing browser.controlFlow – the output should be null.
Refactoring a Login Test to async/await
Below is a minimal, fully working example that logs into an Angular app, waits for navigation, and asserts the page title. Notice the await before every Protractor command.
const { browser, by, element } = require('protractor');
describe('Login flow', () => {
it('should log in and show the dashboard title', async () => {
await browser.get('https://example.com/login');
// Fill credentials
await element(by.id('email')).sendKeys('[contact removed]');
await element(by.id('password')).sendKeys('secret');
// Submit form
await element(by.css('button[type="submit"]')).click();
// Wait for Angular to settle after navigation
await browser.waitForAngular();
// Assert that we are on the dashboard
const title = await element(by.css('h1')).getText();
expect(title).toEqual('Dashboard');
});
});
All commands are now await-ed, so the test reads like regular JavaScript. If you forget an await, the test will hang or produce race conditions because the promise chain will not resolve before the next line runs.
Trade‑offs and Caveats
- Legacy helper libraries (e.g.,
protractor-helper) rely on the Flow and will break. Refactor or replace them. - Third‑party custom matchers that internally use the Flow may fail. Update them to use
async/awaitor plain promises. - Large suites may see a slight performance hit during the transition because each command now returns a real promise.
- Mixing sync and async code can cause unpredictable failures. Ensure every spec uses
async/awaitafter disabling the Flow. - Protractor is in maintenance mode; newer Angular releases may not be fully supported in the next major version.
Actionable Next Steps
- Update
protractor.conf.jswithcontrolFlow: nulland run the test suite to confirm the Flow is disabled. - Run a small subset of specs, refactor them to use
await, and verify they pass. - Gradually replace or rewrite helper libraries and custom matchers.
- Use the provided verification checklist (see below) to ensure every test behaves as expected.
- Once all specs are async/await compliant, consider removing any remaining legacy code and commit the changes.
Verification Checklist
- Run
protractorand confirm the console shows no Control Flow warnings. - In a
beforeEachhook, logbrowser.controlFlow– it should benull. - Execute a spec that contains
await element(by.css('button')).click();and ensure it completes without hanging. - Check stack traces for
async/awaitsyntax rather than legacy.then()chains. - Open Chrome DevTools and verify no requests are blocked due to stale sync commands.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.