Explain Codes LogoExplain Codes Logo

Create a custom callback in JavaScript

javascript
callbacks
async-await
promises
Anton ShumikhinbyAnton ShumikhinยทFeb 3, 2025
โšกTLDR

To create a custom callback in JavaScript, you write a function that accepts another function (the callback function) as an argument. You invoke the callback within your main function when appropriate. Let's jump right in with a code snippet:

function executeCallback(callback) { console.log("[Main] Before callback.") // Of course, you're not just going to trust any random function... if (typeof callback === 'function') { callback(); } else { console.error("[Error] Nice try, but that's not even a function. ๐Ÿ›‘"); } console.log("[Main] After callback.") } executeCallback(() => console.log("[Callback] Hi, I'm the callback! ๐Ÿ‘‹"));

This executeCallback function neatly encapsulates our callback execution. The console outputs "Hi, I'm the callback! ๐Ÿ‘‹", nestled cosily between pre- and post-callback actions.

Callbacks and context: using .call() and .apply()

To manage context or this and to pass arguments to your callback function, use the function.call() or function.apply() methods:

function executeCallbackWithContextAndParams(callback, context, ...params) { if (typeof callback === 'function') { // 'this' is now 'context' and parameters are passed as individual arguments // 'this' says: "New phone, who dis?" ๐Ÿ“ฑ callback.call(context, ...params); } } function exampleCallback() { console.log(`Data processed by '${this.processorName}' processor: ${this.data}`); } executeCallbackWithContextAndParams(exampleCallback, {processorName: 'Awesome', data: 'myData'}, 'arg1', 'arg2');

If you're dealing with an array of arguments, .apply() may be your best friend:

function executeCallbackWithArray(callback, context, params) { if (typeof callback === 'function') { // 'this' is still 'context', but parameters are passed as an array. Array says: "We're in this together!" ๐Ÿ‘ฏ callback.apply(context, params); } } let myArgs = ['param1', 'param2']; executeCallbackWithArray(exampleCallback, {processorName: 'Diligent', data: 'myData2'}, myArgs);

Error handling: bulletproofing your callbacks

Error handling is essential to any robust piece of code. Check to ensure your callback is a function and use a try/catch block to prevent unforeseen errors, especially when callbacks are used with asynchronous operations:

function executeCallbackSafely(callback, ...params) { if (typeof callback === 'function') { try { // Retry logic: try, try and try again. Fingers crossed! ๐Ÿคž callback(...params); } catch (err) { console.error("[Error] Oops! There was an issue executing the callback.๐Ÿคทโ€โ™‚๏ธ", err); } } else { console.warn("[Warning] Callback not executed - it's not a function.๐Ÿšซ"); } } executeCallbackSafely(undefined); // With this function, even a non-function won't break your code. Take that, chaos monkey! ๐Ÿต

Callback in action: handling events and fetching data

Callbacks are handy for handling events or processing data retrieved asynchronously. Thanks to the Event-loop mechanism, JavaScript ensures that callbacks are executed after the completion of their calling function:

function fetchData(url, callback) { fetch(url) .then(response => response.json()) // Call me back when you've got the goods. ๐Ÿ“ž .then(data => callback(data)) .catch(error => console.error(`[Error] Who ate my data? ๐Ÿ”`, error)); } fetchData('https://api.example.com/data', loadedData => { // Data is like pizza. ๐Ÿ• When it arrives, it's time to process (eat) it! console.log(loadedData); });

Organizing callbacks: code structure

Cohesion and modularity is aided by encapsulating callback-related functionality in its own function. Let's refactor our data fetch example with retry logic:

function fetchDataWithRetries(url, callback, retries = 3) { function attemptFetch(remainingTries) { fetch(url) .then(response => response.json()) .then(data => callback(null, data)) // Node.js callback convention: Function callback takes error as the first argument, and result as the second argument. .catch(error => { if (remainingTries > 0) { console.log(`Retrying...๐Ÿ’ช attempts left: ${remainingTries - 1}`); attemptFetch(remainingTries - 1); } else { callback(error, null); } }); } attemptFetch(retries); }

Advanced callbacks: understanding Promises and Async/Await

For more complex tasks involving multiple asynchronous operations, Promises and Async/Await can be used to manage callbacks offering more control and cleaner, more manageable code.

...And action! Automating and testing your callbacks

You can automatically execute and test your callbacks either through testing frameworks like Jest or Mocha or though a custom test runner. By doing so you ensure a robust and reliable functionality of your callbacks:

// Picture a test runner function testRunner('fetchData should retrieve and process data', (done) => { fetchData('https://api.example.com/data', (loadedData) => { assert(loadedData.length > 0, "Data shouldn't be empty, just like my coffee cup before starting a coding session. โ˜•"); done(); }); });