async-await
javascript
sqlite
database
programming

Async await sqlite in javascript

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Using async and await with SQLite makes JavaScript database code much easier to follow, but only if the API you are calling actually returns promises. Many SQLite packages started with callbacks, so the real job is usually to choose a promise-friendly wrapper or convert the callback boundary once and keep the rest of the code consistently asynchronous.

Choose a Promise-Friendly API

A common Node.js setup is sqlite3 together with the sqlite wrapper package. The wrapper gives you promise-based methods such as get, all, run, and exec.

javascript
1import sqlite3 from "sqlite3";
2import { open } from "sqlite";
3
4async function openDb() {
5  return open({
6    filename: "demo.db",
7    driver: sqlite3.Database,
8  });
9}

Because open returns a promise, it fits naturally into async functions.

Running Queries with await

Once the connection is open, you can await the operations directly.

javascript
1const db = await openDb();
2
3await db.exec(`
4  CREATE TABLE IF NOT EXISTS users (
5    id INTEGER PRIMARY KEY,
6    name TEXT NOT NULL
7  )
8`);
9
10await db.run("INSERT INTO users (name) VALUES (?)", "Ava");
11await db.run("INSERT INTO users (name) VALUES (?)", "Noah");
12
13const rows = await db.all("SELECT id, name FROM users ORDER BY id");
14console.log(rows);
15
16await db.close();

This is clearer than nested callbacks because the control flow reads in execution order.

Use the Right Method for the Result Shape

SQLite wrappers usually expose several methods, and each one implies a different expectation:

  • 'get() for one row'
  • 'all() for many rows'
  • 'run() for insert, update, or delete statements'
  • 'exec() for schema setup or multi-statement SQL'
javascript
1const user = await db.get(
2  "SELECT id, name FROM users WHERE name = ?",
3  "Ava"
4);
5
6console.log(user);

Using the correct method is not just style. It makes the code's intent easier to review and avoids awkward post-processing later.

Close the Connection Predictably

A real application should make the connection lifecycle explicit. One clean pattern is to open the database, run the operation inside try, then close it in finally.

javascript
1async function loadUsers() {
2  const db = await openDb();
3  try {
4    return await db.all("SELECT id, name FROM users ORDER BY id");
5  } finally {
6    await db.close();
7  }
8}
9
10console.log(await loadUsers());

This matters because async code has more error exits, and forgotten handles are easy to miss during local testing.

Transactions Still Need to Be Explicit

Using await does not make several statements atomic. If the operations must succeed or fail together, wrap them in a transaction.

javascript
1const db = await openDb();
2
3try {
4  await db.exec("BEGIN");
5  await db.run("INSERT INTO users (name) VALUES (?)", "Mina");
6  await db.run("INSERT INTO users (name) VALUES (?)", "Iris");
7  await db.exec("COMMIT");
8} catch (err) {
9  await db.exec("ROLLBACK");
10  throw err;
11} finally {
12  await db.close();
13}

Without the transaction, the first insert could succeed and the second could fail, leaving the database half-updated.

Wrapping a Callback API Manually

If your project still uses a callback-based SQLite API, convert that boundary once rather than mixing callback style and async and await in the rest of the app.

javascript
1function allAsync(db, sql, params = []) {
2  return new Promise((resolve, reject) => {
3    db.all(sql, params, (err, rows) => {
4      if (err) reject(err);
5      else resolve(rows);
6    });
7  });
8}

Once you have wrappers like this, the rest of your code can stay promise-based and predictable.

Keep the Access Layer Thin

A useful design rule is to keep SQL details inside a small module or repository layer. Then calling code does not need to know whether the implementation uses sqlite3, a wrapper package, or a manual promise adapter.

That boundary also makes testing easier, because higher layers can depend on a plain async interface instead of on raw SQL methods.

Concurrency and Operational Reality

SQLite is lightweight, but it is still a real database. Awaiting queries does not remove its concurrency limits. For long-running services, be deliberate about:

  • how many connections are opened
  • where writes are coordinated
  • when the connection is closed

For many scripts, one connection per run is fine. For a server, opening and closing a database for every tiny operation can become unnecessary overhead.

Common Pitfalls

  • Using await on a callback-based method that does not return a promise.
  • Opening new database connections everywhere with no lifecycle plan.
  • Forgetting to close the database in error paths.
  • Assuming several awaited statements are automatically transactional.
  • Mixing callback style and promise style in the same data-access layer.

Summary

  • 'async and await work well with SQLite when the API is promise-based.'
  • Use a wrapper package or convert callback methods once at the boundary.
  • Choose get, all, run, and exec according to the query shape.
  • Use try and finally so connection cleanup is deterministic.
  • Add explicit transactions whenever multiple awaited statements must succeed together.

Course illustration
Course illustration

All Rights Reserved.