Skip to content

Instantly share code, notes, and snippets.

@danigb
Created April 17, 2018 22:18
Show Gist options
  • Save danigb/d34e0e784809ec7e101711d62d0191b7 to your computer and use it in GitHub Desktop.
Save danigb/d34e0e784809ec7e101711d62d0191b7 to your computer and use it in GitHub Desktop.
promises from scratch

Promises

Me encantan los cursos que son una implementación naive de algo para entenderlo mejor.

What are we going to do

readFile('file.json', (err, data) => console.log(data))


readFile(filename, cb) {
  const data = "MarsBased";
  cb(undefined, data);
}

Step 0: Basic implementation

fetch("file.json").then(data => console.log(data));

function fetch(filename) {
  const data = "MarsBased";
  return {
    then(cb) {
      return Promise(cb(data));
    }
  };
}

Step 1: chainable then -> extract Promise

fetch("file.json")
  .then(data => data.toLowerCase())
  .then(data => console.log(data));

function fetch(filename) {
  return Promise("VenusBased");
}

function Promise(data) {
  return {
    then(cb) {
      return Promise(cb(data));
    }
  };
}

Step 3 - Error handling part 1: catch

Fisrt attempt: fails because it breaks the chain

function fetch(filename) {
  return filename
    ? Promise("MarsBased")
    : Promise(null, "`filename` is required");
}

function Promise(data, err) {
  return {
    then(cb) {
      if (!err) return Promise(cb(data));
    },
    catch(cb) {
      if (err) cb(err);
    }
  };
}
fetch("file.json")
  .then(data => data.toLowerCase())
  .then(data => console.log(data))
  .catch(err => console.log(err));

fetch()
  .then(data => data.toLowerCase())
  .then(data => console.log(data))
  .catch(err => console.log(err));
marsbased
/Users/Dani/MarsBased/Promises/index.js:25
  .then(data => console.log(data))
  ^

TypeError: Cannot read property 'then' of undefined

Second try

fetch("file.json")
  .then(data => data.toLowerCase())
  .then(data => console.log(data))
  .catch(err => console.log(err));

function fetch(filename) {
  return Promise("MarsBased", "A Big Error");
}

function Promise(data, err) {
  return {
    then(cb) {
      if (err) return Promise(null, err);
      else return Promise(cb(data));
    },
    catch(cb) {
      if (err) cb(err);
    }
  };
}

Test 1: catch is not chainable

const promise = new Promise((resolve, reject) => reject("Explode"));

promise
  .then(data => console.log("success", data))
  .catch(err => {
    console.log("error", err);
    return err.toLowerCase();
  })
  .catch(err => console.log("caught 2", err));

Step 4: Promises resolves promises

fetch("file.json")
  .then(data => Promesa(data.toLowerCase()))
  .then(data => console.log(data))
  .catch(err => console.log(err));

function fetch(filename) {
  return filename
    ? Promesa("MarsBased")
    : Promesa(undefined, "`filename` is required");
}

function Promesa(data, err) {
  return {
    then(cb) {
      if (err) {
        return Promesa(undefined, err);
      } else {
        const result = cb(data);
        return result && typeof result.then === "function"
          ? result
          : Promesa(result);
      }
    },
    catch(cb) {
      if (err) cb(err);
    }
  };
}

Step 5: Error handling revisited

fetch("file.json")
  .then(data => {
    throw "Invalid data";
  })
  .then(data => data.toLowerCase())
  .then(data => console.log(data))
  .catch(err => console.log(err));

function fetch(filename) {
  return filename
    ? Promesa("MarsBased")
    : Promesa(undefined, "`filename` is required");
}
function Promesa(data, err) {
  return {
    then(cb) {
      if (err) {
        return Promesa(undefined, err);
      } else {
        try {
          const result = cb(data);
          return result && typeof result.then === "function"
            ? result
            : Promesa(result);
        } catch (err) {
          return Promesa(undefined, err);
        }
      }
    },
    catch(cb) {
      if (err) cb(err);
    }
  };
}

Test 2: chainable catch

fetch("file.json")
  .then(data => {
    throw "Invalid data";
  })
  .then(data => data.toLowerCase())
  .then(data => console.log(data))
  .catch(err => {
    console.log("error", err);
    throw "Invalid error handler";
  })
  .catch(err => console.log("error2", err));

function fetch(filename) {
  return filename
    ? Promesa("MarsBased")
    : Promesa(undefined, "`filename` is required");
}
function Promesa(data, err) {
  return {
    then(cb) {
      if (err) {
        return Promesa(undefined, err);
      } else {
        try {
          const result = cb(data);
          return result && typeof result.then === "function"
            ? result
            : Promesa(result);
        } catch (err) {
          return Promesa(undefined, err);
        }
      }
    },
    catch(cb) {
      try {
        if (err) cb(err);
      } catch (exception) {
        return Promesa(undefined, exception);
      }
    }
  };
}

Step 6: Allow Promise((resolve, reject) => { ... }) syntax

fetch("file.json")
  .then(data => data.toLowerCase())
  .then(data => console.log(data))
  .then(data => {
    throw "Not valid";
  })
  .catch(err => console.log("error:", err));

// Internal API change, no behaviour

function fetch(filename) {
  return filename
    ? Promesa((resolve, reject) => resolve("MarsBased"))
    : Promesa((resolve, reject) => reject("`filename` is required"));
}
// First thing to do
Promesa.resolve = value => Promesa((resolve, reject) => resolve(value));
Promesa.reject = err => Promesa((resolve, reject) => reject(err));

function fetch(filename) {
  return filename
    ? Promesa.resolve("MarsBased")
    : Promesa.reject("`filename` is required");
// Internal and API change. No behaviour change.

function Promesa(executor) {
  let data, err;
  const resolve = value => (data = value);
  const reject = value => (err = value);
  executor(resolve, reject);

  return {
    then(cb) {
      if (err) {
        return Promesa.reject(err);
      } else {
        try {
          const result = cb(data);
          return result && typeof result.then === "function"
            ? result
            : Promesa.resolve(result);
        } catch (err) {
          return Promesa.reject(err);
        }
      }
    },
    catch(cb) {
      try {
        if (err) cb(err);
      } catch (exception) {
        return Promesa.resolve(exception);
      }
    }
  };
}

Interlude (7): welcome to async

Nuestra implementación tiene un defecto sutil. Si ejecutamos el siguiente código:

function fetch(filename) {
  return filename
    ? Promesa.resolve("MarsBased")
    : Promesa.reject("`filename` is required");
}

fetch("file.json").then(data => console.log(data));
console.log("hello");

Obtendremos:

$ node steps/step-6.js
marsbased
hello

Pero lo normal, si fetch utilizase Promises (las verdaderas) obtendríamos ésto:

$ node index.js
marsbased
hello

Con esta nueva sintáxis nos permite escribir un fetch asíncrono, con ayuda de setTimeout:

function fetch(filename) {
  return filename
    ? Promesa(resolve => setTimeout(() => resolve("DATA"), 1000))
    : Promesa.reject("`filename` is required");
}

fetch("file.json").then(data => console.log(data));
console.log("hello");

El problema es que, por desgracia, no acaba de funcionar:

$ time node index.js
hello
error: TypeError: Cannot read property 'toLowerCase' of undefined
    at fetch.then.data

node index.js  0.06s user 0.02s system 7% cpu 1.084 total

Pero como se puede ver hay un avance: hello se ha imprimido antes que el error. Eso significa que cosole.log('hello') se ha evaluado antes que nuestro código y por lo tanto nuestra promesa ha pasado a ser asíncrona. Eso lo corrobora el tiempo que tarda en total el programa en ejecutarse, ejem, en fallar: 1 segundo.

El problema es que cuando la función then de la Promesa ejecuta el callback, el valor de data aún no está definido (porque resolve no se ha ejecutado).

Step 7: embrace async

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment