Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Let's talk straight about Javascript. Repeat with me:

Literally any kind of change to a function in javascript is a breaking change.

Don't believe it? Give me an example of a function and how you change it and I will show you the code that works with the original function but breaks with the changed one.



How do you feel about adding arguments to a "single object as named arguments that is destructured in function declaration"? I can also always come up with something that breaks, but seems pretty safe.

add({a:2, b:4})

function add({a, b}) { return a + b }

function _add({a, b, multiplier = 1}) { return a+b*multiplier }

challenge: come up with a breaking use that does not involve something having a property named "multiplier"


First of all, I _can_ demonstrate code that will work when using "add" but explodes when using the changed version - even with your restriction of not having a "multiplier" property.

However, let's keep the tension for while. The fact that having a working function using "multiplier" is already a bit broken - it shouldn't work from the beginning, but javascript is designed so that it does. Hence you have to give me this restriction, because otherwise it is obvious how your example can be "broken".

Before I expose my (very simple) solution to still break your example even without using "multiplier", I would like to ask you to try to come up with another example first, where you don't require restrictions. I think it's a good exercise. :)

If no one comes up with one, I'll show it in, say, a day from now.


Allright, I didn't really come up with anything clever. I may have been to sloppy in the original function/restriction. Obviously once you start passing non-numbers, all bets are off. E.g. a simple add({a: "hello", b: "world"}) breaks. Add may well have been intended as a string concatination function so yeah, fair enough.

A tricky thing I could see without ever explicitly defining "multiplier" (e.g. on the object prototype) is passing a Proxy that e.g. has a fallback for all missing properties. Detecting a proxy is only kind of possible (?) but we can copy all the original target properties from it, which should make it safe.

So here goes, my safe solution for modifying function signature in a non breaking way:

  function add({ ...args }) {
    const { a, b, ...rest } = args;
    if (typeof a !== "number" || typeof b !== "number") {
      throw "all arguments must be numbers";
    }
    if (Object.keys(rest).length > 0) {
      throw "You may only pass arguments a and b";
    }
    return a + b;
  }

  function _add({ ...args }) {
    const { a, b, multiplier = 1, ...rest } = args;
    if (
      typeof a !== "number" ||
      typeof b !== "number" ||
      typeof multiplier !== "number"
    ) {
      throw "all arguments must be numbers";
    }
    if (Object.keys(rest).length > 0) {
      throw "You may only pass arguments a, b and multiplier";
    }
    return a + b * multiplier;
  }
Most of these issues (not the proxy one) should be solved by typescript.


Okay, you definitely deserve praise for that this prevents problems in a real world scenario. Now I feel a bit bad for having made you write so much code.

In the evil world, I can break your code like that:

    try {
      add(1, 2, 3, 4)
    } catch (e) {
      if(e !== "You may only pass arguments a and b")
        throw "boom";
    }
However, you can of course make your exception string generic.

Then I'll have no choice to use one of my jokers: calling "add.toString()" and inspect your function in detail. Before you scream that this is stupid, please mind that this is actually used out there (looking for example at you, angular).


Oh you wanna have a go? Let's have a go

  function add({ ...args }) {
    const { a, b, ...rest } = args;
    if (typeof a !== "number" || typeof b !== "number") {
      throw "no";
    }
    if (Object.keys(rest).length > 0) {
      throw "no";
    }
    return a + b;
  }

  function _add({ ...args }) {
    const { a, b, multiplier = 1, ...rest } = args;
    if (
      typeof a !== "number" ||
      typeof b !== "number" ||
      typeof multiplier !== "number"
    ) {
      throw "no";
    }
    if (Object.keys(rest).length > 0) {
      throw "no";
    }
    return a + b * multiplier;
  }
  add.toString = () => "nice try";

  _add.toString = () => "nice try";
Edit: OK I think we are stretching HN comment ettiquete to far with this much code. This was fun though. Thanks.


Sure, let's do that :)

    if( Function.prototype.toString.call(add).includes("multiplier") ) throw "boom!";
> Edit: OK I think we are stretching HN comment ettiquete to far with this much code. This was fun though. Thanks.

Huh? Would you mind to educate me about what part of the ettiquete we are not following?


IDK not a real thing, you just never see it. I feel like it's a forum not a chat room and it doesn't collapse deep threads by default so the long code makes the page very long. Probably doesn't matter though.

At this point we can go ahead and break the world:

  add.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;
  _add.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;
  Function.prototype.toString = () => `function add({ ...args }) { const { a, b, ...rest } = args; if (typeof a !== "number" || typeof b !== "number") { throw "no"; } if (Object.keys(rest).length > 0) { throw "no";} return a + b;}`;


I think it's very interesting for others to follow this.

Now, we are leaving the original scope (not just changing a function, but modifying globals). read-only globals even. But prepare for my counter:

    let frame = document.createElement('x');
    document.body.appendChild(frame);
    if( frame.contentWindow.Function.toString.call(add).includes("multiplier")  ) throw "boom!";
You might go to also kill "document.createElement", but there are many ways for me to get a new frame. I think when we come to the point where all these are disabled, I would say only a small fraction of the websites that use javascript would still properly operate. It would be your victory though. ;)


> Before you scream that this is stupid, please mind that this is actually used out there

That is stupid though. It's like saying changing a private field in Java is a breaking change because someone might have used reflection to access it.

Taken to the moronic extreme: any detectable change is a breaking change because someone could write a function that pulls your latest release and depends on every bit being identical with the previous release.


Yeah, it is stupid, but it is also true.

> It's like saying changing a private field in Java is a breaking change because someone might have used reflection to access it.

Which is true, both in theory and practice.

Even look at misc.Unsafe - which is deliberately named unsafe and everyone was told not to use it. Then they tried to drop support for it and people freaked out so much that support was continued. (https://jaxenter.com/java-9-without-sun-misc-unsafe-119026.h...)

> Taken to the moronic extreme: any detectable change is a breaking change because someone could write a function that pulls your latest release and depends on every bit being identical with the previous release.

I would say it is best described here: https://xkcd.com/1172/


Yeah ok, it‘s a saturday in lockdown, why not. I have a hunch that the only way I can do this safely is with a bunch of safety checks. I have to think about wether your breaking has something to do with the intricacies of function arguments in js or if you consider addition of a property to any type of "options" object that is passed around always breaking.

For day to day, I feel this pattern is good enough, as typescript works nicely with it.


const add = ({ a, b }) => a + b; const _add = ({ a, b }) => b + a;


That one is rather simple to break.

    console.log(add({a:"1", b:2})) //12
    console.log(_add({a:"1", b:2})) //21
So to make the code break with the change to _add, I can just do:

    if(add({a:"1", b:2}) != 12) boom()


Ah of course!


How about this? (I am not a 100% about this one)

    f = Object.defineProperties(x => x, {toString:{value:()=>'a'}, [Symbol.toStringTag]:{value:'a'}})
and we will change it to:

    f = Object.defineProperties(y => y, {toString:{value:()=>'a'}, [Symbol.toStringTag]:{value:'a'}})

Edit: ah you -can- break it actually. A challenge for others to figure out how to break this one.


Hehe :) smart one!


To be clear, you mean "literally and kind of change to a function signature [...]", right?


I wished so, but no. It's even true for changes in a function's body. Maybe that's a good hint, no? :)


id = x => x;

logging_id = x => { log(x); return x };


What's log? console.log?


Could be anything, just logging the value of x to some place. Assume it never raises exceptions.


I guess then I have to result in the techniques (toString) that I have discussed in the other thread here!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: