Qt
asynchronous programming
network requests
concatenation
C++

How do nicely concat asynchronous network requests in Qt

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Qt's QNetworkAccessManager performs HTTP requests asynchronously through signals and slots. When you need to chain requests (request B depends on the response of request A), the naive approach of nesting signal connections creates deeply nested, hard-to-read code. Qt offers several patterns to keep chained requests clean: sequential signal-slot connections, state machines, QFuture with Qt Concurrent, and coroutines (Qt 6.5+). The right choice depends on your Qt version and the complexity of the chain.

Basic Async Request in Qt

cpp
1QNetworkAccessManager *manager = new QNetworkAccessManager(this);
2
3QNetworkRequest request(QUrl("https://api.example.com/users"));
4QNetworkReply *reply = manager->get(request);
5
6connect(reply, &QNetworkReply::finished, this, [reply]() {
7    if (reply->error() == QNetworkReply::NoError) {
8        QByteArray data = reply->readAll();
9        qDebug() << "Response:" << data;
10    } else {
11        qDebug() << "Error:" << reply->errorString();
12    }
13    reply->deleteLater();
14});

Problem: Nested Request Chains

cpp
1// Ugly: nested lambdas for sequential requests
2manager->get(QNetworkRequest(QUrl("https://api.example.com/auth")));
3connect(reply1, &QNetworkReply::finished, this, [=]() {
4    QString token = reply1->readAll();
5    reply1->deleteLater();
6
7    QNetworkRequest req2(QUrl("https://api.example.com/users"));
8    req2.setRawHeader("Authorization", ("Bearer " + token).toUtf8());
9    QNetworkReply *reply2 = manager->get(req2);
10
11    connect(reply2, &QNetworkReply::finished, this, [=]() {
12        QJsonArray users = QJsonDocument::fromJson(reply2->readAll()).array();
13        reply2->deleteLater();
14
15        // Third request...nesting goes deeper
16    });
17});

This quickly becomes unreadable with 3+ chained requests.

Solution 1: Sequential Method Calls

Break each request into its own method:

cpp
1class ApiClient : public QObject {
2    Q_OBJECT
3public:
4    void startChain() { authenticate(); }
5
6private:
7    QNetworkAccessManager *m_manager = new QNetworkAccessManager(this);
8    QString m_token;
9
10    void authenticate() {
11        QNetworkReply *reply = m_manager->get(QNetworkRequest(QUrl("https://api.example.com/auth")));
12        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
13            m_token = reply->readAll();
14            reply->deleteLater();
15            fetchUsers();  // Chain to next step
16        });
17    }
18
19    void fetchUsers() {
20        QNetworkRequest req(QUrl("https://api.example.com/users"));
21        req.setRawHeader("Authorization", ("Bearer " + m_token).toUtf8());
22        QNetworkReply *reply = m_manager->get(req);
23        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
24            QByteArray data = reply->readAll();
25            reply->deleteLater();
26            processUsers(QJsonDocument::fromJson(data).array());
27        });
28    }
29
30    void processUsers(const QJsonArray &users) {
31        qDebug() << "Got" << users.size() << "users";
32        emit chainComplete(users);
33    }
34
35signals:
36    void chainComplete(const QJsonArray &users);
37};

Each method handles one request and calls the next on completion. Clean, readable, and easy to insert error handling.

Solution 2: Request Queue

For a variable number of sequential requests:

cpp
1class RequestQueue : public QObject {
2    Q_OBJECT
3public:
4    void enqueue(const QNetworkRequest &request) {
5        m_queue.append(request);
6        if (!m_running) processNext();
7    }
8
9private:
10    QNetworkAccessManager *m_manager = new QNetworkAccessManager(this);
11    QList<QNetworkRequest> m_queue;
12    bool m_running = false;
13
14    void processNext() {
15        if (m_queue.isEmpty()) {
16            m_running = false;
17            emit allComplete();
18            return;
19        }
20
21        m_running = true;
22        QNetworkRequest req = m_queue.takeFirst();
23        QNetworkReply *reply = m_manager->get(req);
24
25        connect(reply, &QNetworkReply::finished, this, [this, reply]() {
26            if (reply->error() == QNetworkReply::NoError) {
27                emit responseReceived(reply->readAll());
28            } else {
29                emit errorOccurred(reply->errorString());
30            }
31            reply->deleteLater();
32            processNext();  // Process next in queue
33        });
34    }
35
36signals:
37    void responseReceived(const QByteArray &data);
38    void errorOccurred(const QString &error);
39    void allComplete();
40};

Solution 3: QFuture with QtConcurrent (Qt 6)

Qt 6 introduced QPromise and .then() chaining:

cpp
1#include <QPromise>
2#include <QFuture>
3
4QFuture<QByteArray> asyncGet(QNetworkAccessManager *manager, const QUrl &url) {
5    QPromise<QByteArray> promise;
6    QFuture<QByteArray> future = promise.future();
7
8    QNetworkReply *reply = manager->get(QNetworkRequest(url));
9    QObject::connect(reply, &QNetworkReply::finished, [reply, promise = std::move(promise)]() mutable {
10        if (reply->error() == QNetworkReply::NoError) {
11            promise.addResult(reply->readAll());
12        } else {
13            promise.setException(std::make_exception_ptr(std::runtime_error(reply->errorString().toStdString())));
14        }
15        promise.finish();
16        reply->deleteLater();
17    });
18
19    return future;
20}
21
22// Chain with .then()
23asyncGet(manager, QUrl("https://api.example.com/auth"))
24    .then([manager](const QByteArray &token) {
25        QNetworkRequest req(QUrl("https://api.example.com/users"));
26        req.setRawHeader("Authorization", "Bearer " + token);
27        return asyncGet(manager, req.url());
28    })
29    .then([](const QByteArray &data) {
30        qDebug() << "Users:" << data;
31    })
32    .onFailed([](const std::exception &e) {
33        qDebug() << "Error:" << e.what();
34    });

Solution 4: Coroutines (Qt 6.5+ with C++20)

cpp
1// Requires C++20 coroutine support and QCoro library
2#include <QCoro/QCoroNetworkReply>
3
4QCoro::Task<void> fetchData(QNetworkAccessManager *manager) {
5    // First request
6    auto *reply1 = manager->get(QNetworkRequest(QUrl("https://api.example.com/auth")));
7    co_await reply1;
8    QString token = reply1->readAll();
9    reply1->deleteLater();
10
11    // Second request — reads like synchronous code
12    QNetworkRequest req(QUrl("https://api.example.com/users"));
13    req.setRawHeader("Authorization", ("Bearer " + token).toUtf8());
14    auto *reply2 = manager->get(req);
15    co_await reply2;
16    QByteArray users = reply2->readAll();
17    reply2->deleteLater();
18
19    qDebug() << "Users:" << users;
20}

Coroutines make async code look sequential, eliminating callback nesting entirely.

Common Pitfalls

  • Not calling deleteLater() on replies: QNetworkReply objects are not automatically deleted. Forgetting reply->deleteLater() causes memory leaks. Always delete the reply after reading its data.
  • Capturing this in lambdas when the object may be destroyed: If the parent QObject is destroyed before the reply finishes, the lambda captures a dangling pointer. Use QPointer<QObject> or ensure the object's lifetime exceeds the network request.
  • Blocking the event loop with QEventLoop: Using QEventLoop to make a synchronous wrapper around async requests blocks the entire UI thread. Use signal chains, futures, or coroutines instead.
  • Not handling errors at each step: If request A fails but the code unconditionally starts request B, the chain produces confusing errors. Check reply->error() at each step and abort the chain on failure.
  • Creating a new QNetworkAccessManager per request: QNetworkAccessManager manages connection pooling and cookie storage. Creating a new one per request loses connection reuse and session state. Share one instance across the chain.

Summary

  • Break chained requests into separate methods that call each other on completion
  • Use a request queue for a variable number of sequential requests
  • Qt 6's QFuture::then() enables promise-style chaining
  • QCoro coroutines (Qt 6.5+ with C++20) make async code look synchronous
  • Always call reply->deleteLater() after processing each response
  • Share a single QNetworkAccessManager across all requests for connection pooling

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.