Eda Eren

January 13, 2024
  • JavaScript

When in need of a Promise

If you're using a library that uses callbacks extensively, like sqlite3, there's a good chance that it creates some frustration. There are libraries that add a promise-based API to sqlite3, such as sqlite that might be more convenient to use, but let's narrow our focus for now and take a look at a very simple (and somewhat contrived) case.

Let's say we have an insertRow function that simply inserts data into some entries table:

async function insertRow(db, data) {
const { title, description, date } = data;

db.run(
`INSERT INTO entries(title, description, date) VALUES(?, ?, ?);`,
title,
description,
date,
async function (error) {
if (error) {
throw new Error(error.message);
} else {
console.log(
`Inserted a row to entries with the ID: ${this.lastID}`
);
}
}
);
}
async function insertRow(db, data) {
const { title, description, date } = data;

db.run(
`INSERT INTO entries(title, description, date) VALUES(?, ?, ?);`,
title,
description,
date,
async function (error) {
if (error) {
throw new Error(error.message);
} else {
console.log(
`Inserted a row to entries with the ID: ${this.lastID}`
);
}
}
);
}

...which is called by insert:

async function insert(data) {
const db = new sqlite3.Database(filepath);
try {
await insertRow(db, data);
} catch (error) {
throw new Error(error.message);
}
}
async function insert(data) {
const db = new sqlite3.Database(filepath);
try {
await insertRow(db, data);
} catch (error) {
throw new Error(error.message);
}
}

...which is called by createEntry:

async function createEntry(formData) {
const data = {
title: formData.get('title'),
description: formData.get('description'),
date: formData.get('date'),
};

try {
await insert(data);
} catch (error) {
return { message: `Failed to create entry ${data.title}` };
}
}
async function createEntry(formData) {
const data = {
title: formData.get('title'),
description: formData.get('description'),
date: formData.get('date'),
};

try {
await insert(data);
} catch (error) {
return { message: `Failed to create entry ${data.title}` };
}
}

It might indeed be a contrived example, but if you're quick to notice, we're trying to rethrow the error (which was first thrown by insertRow) in insert, and catch it in createEntry. However, in insertRow, the error is thrown inside the callback, which is passed to db.run — which means it will never be caught, unless we do something about it.

Instead, we can return a promise from insertRow that rejects when an error happens:

async function insertRow(db, data) {
const { title, description, date } = data;

return new Promise((resolve, reject) => {
db.run(
`INSERT INTO entries(title, description, date) VALUES(?, ?, ?);`,
title,
description,
date,
async function (error) {
if (error) {
reject(error);
} else {
console.log(
`Inserted a row to entries with the ID: ${this.lastID}`
);
resolve();
}
}
);
});
}
async function insertRow(db, data) {
const { title, description, date } = data;

return new Promise((resolve, reject) => {
db.run(
`INSERT INTO entries(title, description, date) VALUES(?, ?, ?);`,
title,
description,
date,
async function (error) {
if (error) {
reject(error);
} else {
console.log(
`Inserted a row to entries with the ID: ${this.lastID}`
);
resolve();
}
}
);
});
}

Since we're using try...catch in insert and rethrow the error, we'll eventually return with

{ message: `Failed to create entry ${data.title}` };
{ message: `Failed to create entry ${data.title}` };

inside createEntry as we catch the error there.

This might not be ideal, and using a library that provides a promise-based API would be a better idea, but when you have to use a callback-based approach, wrapping it as a Promise is something to keep in mind.