When writing JavaScript code, you’ll often encounter a situation where you need to pass multiple arguments to a callback function, but it only accepts one. Today, we’ll look at three ways to solve this problem. Let’s get started!

The problem

Let’s look at a common example: adding an event listener. Because the .addEventListener() method passes one argument (the Event object) into our callback function, we can just provide the name of a function with a single parameter. Note that we aren’t calling the function; we’re telling the .addEventListener() method which function to call when the event occurs.

const button = document.querySelector("button");

function handleClick(event) {
console.log(event);
}

button.addEventListener("click", handleClick);

What if we want to pass extra arguments to the handleClick() function? The following won’t work:

const button = document.querySelector("button");

function handleClick(foo, event) {
console.log(foo, event);
}

button.addEventListener("click", handleClick("bar", event));

There are two problems with this. Firstly, we’re passing in the value of the global .event property. Not only is this property deprecated, but the value is undefined in this context anyway. We’re effectively passing undefined to the second parameter of the handleClick() function:

button.addEventListener("click", handleClick("bar", undefined));

Secondly, the implicit return value of the handleClick() function is undefined, so we’re effectively passing undefined to the second parameter of the .addEventListener() method:

button.addEventListener("click", undefined);

How can we solve this?

Solution: Use a wrapper function

Perhaps the most obvious solution is to use a wrapper function. The wrapper function receives a single argument, and in the function body, you call another function with multiple arguments.

const button = document.querySelector("button");

function fn(foo, event) {
console.log(foo, event);
}

button.addEventListener("click", function (event) {
fn("bar", event);
});

You’ll often see this written as an anonymous function expression as above, but function expressions can also be named:

button.addEventListener("click", function handleClick(event) {
fn("bar", event);
});

In this case, it might be cleaner to switch from a function expression to a function declaration and pass in the name of the function as before:

function handleClick(event) {
fn("bar", event);
}

button.addEventListener("click", handleClick);

If you want to write the event listener on a single line, you can use an arrow function. However, you should note that this will change the value of the this keyword. In the body of an event handler, the value of the this keyword is the element to which you attached the listener: an HTMLButtonElement in this case. Arrow functions don’t have their own binding for the this keyword; they use the value of the this keyword from their lexical scope.

button.addEventListener("click", (event) => handleClick("bar", event));

I would also advise you to use the void operator to prevent a leaking arrow function. The event listener callback function is not expected to return a value, so the void operator makes it clear that you don’t intend to use the implicit return value of the arrow function. The return value may already be undefined, but this is still a side effect that you need to be aware of. The void operator makes your intention clear.

button.addEventListener("click", (event) => void handleClick("bar", event));

Solution: Use a bound function

The second option is to use the Function.prototype.bind() method to create a bound function. According to the MDN Web Docs:

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

const button = document.querySelector("button");

function handleClick(foo, event) {
console.log(foo, event);
}

button.addEventListener("click", handleClick.bind(button, "bar"));

To maintain the HTMLButtonElement as the value of the this keyword, we use the value of the button constant as the first argument to the .bind() method. Then we set up the string value "bar" as the first argument to the bound function. Finally, we use the returned function as the event handler. When the .addEventListener() method invokes the bound function, it passes in the Event object after the first argument ("bar").

Solution: Use a higher-order function

A higher-order function is a function that takes one or more functions as arguments and/or returns another function. In this case, it’s the latter. The handleClick() function accepts a value via its foo parameter, then returns a function to be used as the event handler. Because the inner function is in the lexical scope of the outer function, it has access to the value of foo.

const button = document.querySelector("button");

function handleClick(foo) {
return function (event) {
console.log(foo, event);
};
}

button.addEventListener("click", handleClick("bar"));

This is like the wrapper function in reverse. When we use the wrapper function, we’re telling the .addEventListener() method to call it at a later time. When called, the wrapper function invokes another function. When we use the higher-order function, we actually do call it right away, and we’re telling the .addEventListener() method to call the returned function at a later time.

I only learned this technique recently, but I love it. I can see why people like functional programming. It feels very elegant. Astute readers might notice that the .bind() method is a higher-order function too, because it also returns a function!

Which technique should you use?

As with most things, the answer is “it depends.” You just need to weigh up the pros and cons and use whatever is best for your situation.

  • I personally feel that the wrapper function is the easiest to understand, especially for beginners. There’s nothing wrong with it and I still use it frequently.
  • The bound function is most useful when you need it to always be called with the same value for the this keyword; using it to pass extra arguments feels like “cheating,” at least to me. I realize how silly this sounds. It's fine!
  • The higher-order function might be my new preference, but I personally feel it’s harder to understand. You may feel differently if you come from a mathematical background or are used to functional programming.

What’s your favourite technique? I’d love to know!