6

Recently I needed to use RxJS. I tried to design an error handling flow, but I discovered some weird syntax passing method arguments:

.subscribe(
    x => {
    },
    console.warn // <- Why does this compile, and warn 'is not 7' in debug console?
);

Link to minimal reproduction:

https://stackblitz.com/edit/rxjs-6-5-error-handle-no-arrow-issue

Steps to reproduce:

  1. Use RxJS 6.5
  2. Create a function return observable
  3. Subscribe to observable
  4. Pass parameter into subscribe
  5. Just use ,console.warn, not like ,error => { console.warn(error); }

Without an arrow function, it still passes errors to console.warn. Why?

Code:

import { throwError, concat, of } from "rxjs";
import { map } from "rxjs/operators";

const result = concat(of(7), of(8));

getData(result).subscribe(
  x => {
    console.log("typeof(x)", typeof(x));
    if (typeof(x) === 'string') {
      console.log("x  Error", x);
      return;
    }
    console.log("no error", x);
  },
  console.warn // <- Why does this compile, and warn 'is not 7' in debug console?
);

// pretend service method
function getData(result) {
  return result.pipe(
    map(data => {
      if (data !== 7) {
        throw "is not 7";
      }
      return data;
    })
  );
}

I tried to google some keywords, js, rxjs, angular, omit arrow function, argument missing,... but I cannot locate what tech is used here.

Could anyone provide links to where this mechanism is explained?

The following two questions are related but do not explain the behavior, just say "equivalent":

The line

map(this.extractTreeData)

is the equivalent of

map(tree => this.extractTreeData(tree))

How to pass extra parameters to RxJS map operator

Why is argument missing in chained Map operator

Boann
  • 48,794
  • 16
  • 117
  • 146
Paul0000
  • 71
  • 1
  • 4
  • 3
    functions are "first class objects" in javascript. it doesn't matter whether they are declared "inline" or referenced via a variable – Sam Mason Jul 31 '20 at 07:38
  • 2
    `console.warn` is a function, it expects a function, so it's happy – Liam Jul 31 '20 at 07:46
  • thanks to everyone leaves the message when I ask question, let me figure out, I came from Java 8, and especially "functions are first class objects" is what I need – Paul0000 Jul 31 '20 at 09:54

5 Answers5

10

First you need to understand what you're actually passing to the .subscribe function. Essentially it accepts three optional arguments next, error and complete. Each of it is a callback to be executed when the corresponding notification is emitted by the source observable.

So when you're using an arrow function, you define an in-place callback function.

sourceObservable.subscribe({
  next: (value) => { },
  error: (error) => { },
  complete: () => { }
});

Instead you could define the functions separately and use it as callbacks.

onNext(value) {
}

onError(error) {
}

onComplete() {
}

sourceObservable.subscribe({
  next: this.onNext,
  error: this.onError,
  complete: this.onComplete
});

Now this is what you're seeing. But instead of a user-defined function, you're passing the built-in console.warn() function. And in-turn the values from the notifications will be passed as arguments to the callback functions. So the value from your error is not 7 is sent as argument to console.warn() which then does it's job (i.e. prints to the console).

However there's a catch. If you wish to refer to any of the class member variables using the this keyword in the callback, it'll throw an error saying the variable is undefined. That's because this refers to the scope of the function in the callback and not the class. One way to overcome this is to use an arrow function (we've seen that already). Or use bind() function to bind the meaning of this keyword to the class.

sourceObservable.subscribe({
  next: this.onNext.bind(this),
  error: this.onError.bind(this),
  complete: this.onComplete.bind(this)
});

So if you wish to only have the error callback for example, you could explicitly state it and ignore the others.

sourceObservable.subscribe({ error: console.warn });

Now as to your question "why no parentheses in the function call", it was discussed here and here. The arguments expect a reference to a function and the function names denotes their reference.

ruth
  • 29,535
  • 4
  • 30
  • 57
5

console.log is a function

function can be called with arguments in a bracket

console.log("123") means call function console.log with argument "123"

tree => console.log(tree) is also a function

it can also be called with arguments in a bracket, eg. (tree => console.log(tree))(tree)

so a function with callback as its argument can call its callback with arguments in a bracket

function example(callback) {
callback();
}

so if we pass console.log to it, example(console.log), it basically runs as

function example(callback) {
console.log();
}

if we pass tree => console.log(tree) to it, example(tree => console.log(tree)), it basically runs as

function example(callback) {
(tree => console.log(tree))();
}

if you understood above code. it's easy to understand subscribe now.

function subscribe(nextCb, errorCb, completeCb) {
// ... got next data
nextCb(data);
//... got error
errorCb(error);
// completed observe
completeCb();
} 

so your error callback console.log basically get called as console.log(error);

error=> console.log(error) basically get called as (error=> console.log(error))(error);

which in this case results are same.

Pac0
  • 21,465
  • 8
  • 65
  • 74
arslan2012
  • 1,010
  • 7
  • 19
  • The OP is passing it as `console.warn`(no brackets) so you haven't actually covered their scenario – Liam Jul 31 '20 at 07:47
2

In JS functions are first class objects. When you have the code console.warn no brackets you have a reference to this object but you're not invoking that object, that would require braces console.warn(). For example you can do:

let x = console.warn;
console.log('not happened yet');
x('test');

So your code is simple passing the console.warn function to the parameter of the Subscribe failure in exactly the same manner as you might pass any other function, e.g.

Subscribe(() => {}, () => {});

[why] show Warn 'is not 7'

The other part of this is that your throwing an error throw "is not 7";. The signature of the error call of Subscribe is thus:

subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription;

So the parameter of error is of type any. So the throw passes an Error to the error function handler. This is set as console.warn which has a signature of :

console.warn(obj1 [, obj2, ..., objN]);

console.warn essentially turns whatever parameter it's passed into a string, JS is not strongly typed and this is essentially down to type coercion, and logs it. the string of throw "is not 7"; is is not 7. So it logs is not 7.

All in all I'd say this is all all a bit cryptic and potentially difficult to follow. There is nothing technically wrong here, but I would say it would make more sense to do the following:

.subscribe(
    x => {
    },
    x => {console.warn(x);} 
);

Based on the principle that "Any fool can write code that a computer can understand. Good programmers write code that humans can understand."

Liam
  • 27,717
  • 28
  • 128
  • 190
0

This happens due to the three possible types of value an Observable is able to emit,

  1. next
  2. error
  3. complete

These logic is translated in the arguments of the subscription function, so the first function callback will trigger the values emitted via next, the second callback the values emitted with error and the third function with the complete value.

In you case, console.warn is passed to the second function as a function that will be called each time an error is emitted.

For the second question you can refer to the arrow function docs, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

bernatsampera
  • 192
  • 1
  • 8
-1

Your subscribe expects 2 arguments. The first argument is a function that will be called with the "next" values and the second argument is again a function that will be called with if there occured an error. So, since "console.warn" is a function, you can just use it as a second argument.

(error) => console.warn(error) is the same as console.warn(error)

But be careful if since console.warn does not rely on the context "this", you will give no issues. But if you want to use a function that uses the context "this", you will need to use an arrow function.

For more info about JS scoping: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

Pac0
  • 21,465
  • 8
  • 65
  • 74
J. Pinxten
  • 307
  • 3
  • 7
  • 2
    "`(error) => console.warn(error)` is the same as `console.warn(error)`": no, it's not. `(error) => console.warn(error)` is roughly the same as `console.warn` (hence, OP's question) – Pac0 Jul 31 '20 at 07:42