A callback function in TypeScript is a function that we pass as an argument to another function, which calls it later, often when an asynchronous task such as a timer or an HTTP request has finished. The receiving function decides when the callback runs and which arguments it gets, and TypeScript checks both sides with a function type such as (name: string, age: number) => void. Callbacks are the oldest way to handle asynchronous work in JavaScript; Promises and async/await are built on the same idea and are what we use for most new code.
We use callbacks whenever code must react to something that happens later, for example a button click in a web page or a finished file read in a Node.js script.
The following example shows a callback type, a function that calls the callback with arguments, and a call with an arrow-function callback, followed by a closure that passes our own values and the same task written with a Promise.
// 1. Callback type
type OnLoaded = (name: string, age: number) => void;
// 2. The function calls the callback with arguments
function loadAge(name: string, done: OnLoaded): void {
setTimeout(() => done(name, 37), 100);
}
// 3. Pass a callback
loadAge("Lokesh", (name, age) => console.log(name + " is " + age)); // Lokesh is 37
// 4. Pass our own values with a closure
const greeting = "Hi";
setTimeout(() => console.log(greeting + ", Raj"), 50); // Hi, Raj
// 5. Same task with a Promise and async/await
function loadAgeAsync(name: string): Promise<number> {
return new Promise((resolve) => setTimeout(() => resolve(37), 100));
}
const age = await loadAgeAsync("Lokesh"); // age = 37
Notice that the callback in step 3 gets its arguments from loadAge(), whereas the closure in step 4 reads greeting from the surrounding code.
Next, we type a callback, pass parameters to it (from the calling function, through a closure, with bind(), or through setTimeout()), and look at Node.js-style error-first callbacks. After that, we turn the same callback code into a Promise and into async/await, compare the styles, and cover the this problem that appears when a class method is passed as a callback.
1. Synchronous and Asynchronous Callbacks
Not every callback is asynchronous. Array methods such as map() and filter() call their callback right away, once per element, and return only after the last call, whereas a timer or a network request calls its callback later, after the current code has finished running. The two kinds are called synchronous and asynchronous callbacks.
// 1. Synchronous callback: runs before map() returns
const ages = [37, 35, 40];
const next = ages.map((age) => age + 1); // next = [38, 36, 41]
// 2. Asynchronous callback: runs after the current code finishes
console.log("1. before");
setTimeout(() => console.log("3. callback"), 100);
console.log("2. after");
In step 2, the callback line prints after the “after” line, because JavaScript runs on one thread. The call setTimeout() only registers the callback and returns at once, and the event loop calls the callback after 100 ms, when the thread is free. For example, a function that loads a user profile over HTTP cannot read the profile on the line after the request. Code that needs the result of an asynchronous task must run inside the callback, or after an await, and not on the line after the call.
2. Typing a Callback Parameter
A callback parameter needs a function type that lists the callback’s parameters and return type. For example, a CSV import function takes an onRow callback typed as (row: string[], line: number) => void, so every caller gets the row and its line number. There are three ways to write the type, and all of them produce the same check.
- Write the type inline in the parameter list.
- Give the type a name with a type alias.
- Describe the type with an interface that has a call signature.
// 1. Inline type
function forEachFruit(fruits: string[], cb: (fruit: string, index: number) => void): void {
for (let i = 0; i < fruits.length; i++) {
cb(fruits[i], i);
}
}
forEachFruit(["apple", "banana"], (fruit, i) => console.log(i + ": " + fruit)); // 0: apple, 1: banana
// 2. Type alias
type AgeCallback = (name: string, age: number) => void;
const printAge: AgeCallback = (name, age) => console.log(name + " " + age);
printAge("Raj", 35); // Raj 35
// 3. Interface with a call signature
interface AgeFilter {
(age: number): boolean;
}
const isAdult: AgeFilter = (age) => age >= 18;
const adults = [37, 12, 40].filter(isAdult); // adults = [37, 40]
We did not annotate the callback parameters in this snippet, because TypeScript takes their types from the callback type (this is called contextual typing), so fruit is a string and i is a number.
A return type of void means the caller ignores the return value, but it does not forbid a callback that returns something, which is why ages.forEach((a) => list.push(a)) compiles even though push() returns a number. We avoid the type Function for callbacks, because it accepts any function and checks no arguments.
3. Passing Parameters to a Callback Function
There are two different questions behind “passing parameters to a callback”, and TypeScript checks both.
- Which arguments the callback receives from the function that calls it. The function type answers that.
- How we get our own extra values into a callback when the caller passes only its fixed arguments.
3.1. Arguments From the Calling Function
The function that receives the callback decides which arguments it passes, so our job is to accept those arguments in that order. If our callback declares a different type for a parameter, the call fails to compile.
function loadAge(name: string, done: (name: string, age: number) => void): void {
setTimeout(() => done(name, 37), 100);
}
loadAge("Lokesh", (name, age) => console.log(name + " is " + age)); // Lokesh is 37
When we write the callback as (name: string, age: string) => …, the compiler rejects it with TS2345: Argument of type ‘(name: string, age: string) => void’ is not assignable to parameter of type ‘(name: string, age: number) => void’. A callback may declare fewer parameters than it receives, such as (name) => …, and then it ignores the extra arguments. Because a callback can always ignore an argument, callback types should not declare such parameters as optional parameters.
3.2. Our Own Values: Closure, bind() and setTimeout() Arguments
The function setTimeout() calls its callback with no arguments. For example, a reminder app that schedules a message for each user needs the user’s name inside the timer callback. When the callback needs values, we have three options.
- A closure, which is the most common option, is an arrow function that calls our function with the values it can see in the surrounding code.
- The method Function.prototype.bind() creates a new function with the first arguments already fixed.
- The function setTimeout() itself accepts extra arguments after the delay and passes them to the callback.
function greet(greeting: string, name: string): void {
console.log(greeting + ", " + name);
}
// 1. Closure: an arrow function that calls greet with our values
setTimeout(() => greet("Hello", "Lokesh"), 50); // Hello, Lokesh
// 2. bind(): a new function with the first arguments fixed
const sayHi = greet.bind(null, "Hi");
setTimeout(() => sayHi("Raj"), 100); // Hi, Raj
// 3. setTimeout passes extra arguments to the callback
setTimeout(greet, 150, "Hey", "John"); // Hey, John
TypeScript type-checks all three. In strict mode, the type of sayHi is (name: string) => void, and setTimeout(greet, 150, “Hey”, 5) fails to compile because 5 is not a string. The closure is the one we use most, because it reads like a normal call.
A frequent mistake is writing setTimeout(greet(“Hi”, “Raj”), 100), which calls greet() right away and passes its return value, undefined, to setTimeout(). TypeScript catches the mistake with the error TS2345: Argument of type ‘void’ is not assignable to parameter of type ‘() => void’.
3.3. Generic Callbacks
When a function works with any element type, a generic type parameter connects the data and the callback, so the callback gets the exact element type of each call.
function each<T>(items: T[], cb: (item: T, index: number) => void): void {
items.forEach(cb);
}
each([37, 35], (age, i) => console.log(i, age.toFixed(1))); // 0 37.0, 1 35.0
each(["apple"], (fruit) => console.log(fruit.toUpperCase())); // APPLE
In the first call, T is number, so age.toFixed() compiles. In the second, T is string, and fruit has every string method.
4. Error-First Callbacks
An asynchronous task can fail, and a callback needs a way to receive the error. Node.js core APIs use the error-first convention, where the first callback parameter is either an Error or null, and the result follows. The callback checks the error before it uses the result. For example, fs.readFile() calls its callback with an error when the file does not exist.
const ages = new Map([["Lokesh", 37], ["Raj", 35]]);
type AgeCallback = (err: Error | null, age?: number) => void;
function findAge(name: string, cb: AgeCallback): void {
setTimeout(() => {
const age = ages.get(name);
if (age === undefined) {
cb(new Error("Not found: " + name));
} else {
cb(null, age);
}
}, 100);
}
findAge("Raj", (err, age) => {
if (err) {
console.log(err.message);
return;
}
console.log("Raj is " + age); // Raj is 35
});
findAge("John", (err, age) => {
if (err) {
console.log(err.message); // Not found: John
return;
}
console.log("John is " + age);
});
We cannot catch this error with try/catch around findAge(), because the function returns before the timer fires, so any exception thrown inside the timer callback happens later, outside the try block. For this reason, findAge() passes the error to the callback as an argument.
The error-first pattern has two costs. Every callback repeats the if (err) check, and nothing forces us to write it. When one task depends on the result of another, the callbacks also nest inside each other, a shape often called callback hell. Promises solve both problems.
5. From Callbacks to Promises
A Promise is an object that represents a result that will be available later, and it is either fulfilled with a value or rejected with an error. We can wrap any callback API in a Promise, where the callback calls either resolve() with the result or reject() with the error.
// 1. Wrap a callback API in a Promise
function findAgeAsync(name: string): Promise<number> {
return new Promise((resolve, reject) => {
findAge(name, (err, age) => {
if (err) {
reject(err);
} else {
resolve(age!);
}
});
});
}
// 2. Use it with then() and catch()
findAgeAsync("Lokesh").then((age) => console.log("then: " + age)); // then: 37
findAgeAsync("John").catch((err: Error) => console.log("catch: " + err.message)); // catch: Not found: John
The return type Promise<number> tells every caller what the result is, and errors reach the caller through catch() instead of an err parameter. We need age! because the callback type marks age optional, and in the success branch we know it is set.
For callback functions that fully follow the error-first convention, Node.js has util.promisify(), imported from node:util. It builds the wrapper for us, and the TypeScript types for Node.js infer its signature.
const findAgeP = promisify(findAge); // (arg1: string) => Promise<number | undefined>
const age = await findAgeP("Raj"); // age = 35
The inferred result is number | undefined because the callback’s age parameter is optional, whereas a hand-written wrapper such as findAgeAsync() gives us a tighter type.
6. async and await
An async function always returns a Promise, and inside it, await pauses the function until a Promise settles. The code reads from top to bottom like synchronous code, and we handle errors with ordinary try/catch. A small wait() helper turns setTimeout() into a Promise so that we can await a delay.
// 1. Delay helper that returns a Promise
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const ages = new Map([["Lokesh", 37], ["Raj", 35]]);
async function findAge(name: string): Promise<number> {
await wait(100);
const age = ages.get(name);
if (age === undefined) {
throw new Error("Not found: " + name);
}
return age;
}
// 2. Sequential calls read top to bottom
const lokesh = await findAge("Lokesh"); // lokesh = 37
const raj = await findAge("Raj"); // raj = 35
// 3. Parallel calls with Promise.all()
const both = await Promise.all([findAge("Lokesh"), findAge("Raj")]); // both = [37, 35]
// 4. Errors with try/catch
try {
await findAge("John");
} catch (err) {
console.log((err as Error).message); // Not found: John
}
Step 2 waits about 200 ms in total, because the second call starts after the first one finishes. Step 3 starts both calls at once with Promise.all() and waits about 100 ms. We use the parallel form whenever the calls do not depend on each other, for example when a dashboard loads the user profile and the latest orders.
In step 4, the type of err is unknown, not Error, because JavaScript code can throw any value. The cast is the short form, and an instanceof check is the safer form for exception handling in TypeScript. Node.js also ships a ready-made Promise-based timer in node:timers/promises (the Timers Promises API); our wait() helper does the same with no import.
Callbacks do not disappear with async/await. An async arrow function can itself be the callback, for example in map(), which returns one Promise per element.
const names = ["Lokesh", "Raj"];
const upper = await Promise.all(
names.map(async (name) => {
await wait(50);
return name.toUpperCase();
}),
); // upper = ["LOKESH", "RAJ"]
Passing an async callback to forEach() is a common bug, because forEach() ignores the returned Promises, so the code after the loop runs before the callbacks finish. With map() and Promise.all(), or a for…of loop with await, the order is guaranteed.
6.1. Callbacks vs Promises vs async/await
All three styles run the same asynchronous work. They differ in how results and errors reach our code, which affects how readable the code stays as it grows.
| Point | Callbacks | Promises with then() | async/await |
|---|---|---|---|
| Result delivery | Callback arguments | then() handler | Return value of await |
| Error handling | err argument, checked by hand | catch() handler | try/catch |
| Steps that depend on each other | Nested callbacks | Chained then() calls | Lines one after another |
| Parallel work | Manual counters | Promise.all() | await Promise.all() |
| Where we still use it | Events, array methods, older Node.js APIs | Library code, combining Promises | Most new application code |
7. Losing this When Passing a Method as a Callback
A class method that uses this depends on how it is called. When we pass counter.addMethod as a callback, only the function is passed, without the object, so this is undefined inside it (classes always run in strict mode). TypeScript does not report this mistake. For example, passing a method such as cart.addItem to addEventListener() has the same problem, because the method no longer runs on the cart object.
class Counter {
count = 0;
// 1. Method: loses "this" when passed as a callback
addMethod(): void {
this.count++;
}
// 2. Arrow function property: keeps "this"
addArrow = (): void => {
this.count++;
};
}
const counter = new Counter();
[1, 2, 3].forEach(counter.addArrow);
console.log(counter.count); // 3
[1, 2].forEach(() => counter.addMethod()); // wrap the method in an arrow
console.log(counter.count); // 5
[1].forEach(counter.addMethod); // compiles, fails at runtime
The last line throws TypeError: Cannot read properties of undefined (reading ‘count’). There are three fixes, and the snippet shows the first two.
- Declare the method as an arrow function property, which captures this when the object is created.
- Wrap the method in an arrow function at the call site.
- Pass a bound method, as in counter.addMethod.bind(counter).
8. Running the Callback Examples
The example code on GitHub has one file per section and an index.ts that runs them one after another, waiting for each section’s timers to finish. The project needs TypeScript 7.0.2 in strict mode and Node.js 22 or newer; the promisify() example uses the Node.js type definitions from @types/node.
npm install
npm start
The printed values match the snippet comments. Timers print in the order of their delays, so in the quick reference “Hi, Raj” (50 ms) appears before “Lokesh is 37” (100 ms).
9. Conclusion
A callback is a function passed to another function and called by it, right away (map(), filter()) or later (timers, events, I/O). TypeScript types the callback with a function type, and the calling function decides which arguments it receives; our own values reach the callback through a closure in most cases, or through bind() and the extra arguments of setTimeout(). Error-first callbacks report failures as the first argument, but nest badly. Wrapping them in a Promise, by hand or with util.promisify(), lets us use async/await with plain try/catch, which is the style we use for new asynchronous code.
10. References
MDN documents the JavaScript runtime behavior; the Node.js documentation covers promisify() and the Promise-based timers.
- MDN: Callback function
- MDN: Using promises
- MDN: async function
- MDN: setTimeout()
- MDN: Function.prototype.bind()
- Node.js: util.promisify()
- Node.js: JavaScript asynchronous programming and callbacks
Happy Learning !!
Callback function in JavaScript
in the Senior we have keep set timeout
if you remove 2 sec there is no use