Upgrading from drupal 8 to drupal 9 needs to be done with composer. This guiding screencast and blog post can guide you.

Screencast

Prerequisites

Step 1: remove drupal/console

For a starter, make sure you remove drupal/console first (!). This will prevent you from having a loop in dependencies for the symfony package:

composer remove drupal/console

Step 2: remove drupal/core and/or drupal/core-recommended from composer.json

Look for drupal/core-recommended or drupal/core in composer.json and remove the line(s):

"drupal/core-recommended": "^8.9.0"

Then run composer update again:

composer update

This will remove drupal core from your installation, but update the necessary contrib that is causing a lot of inconsistencies and package constraint sh*it ;).

If you have drush and devel, which are widely used, also manually edit the versions:

"drupal/devel": "^4.0",
"drush/drush": "^10.0.0",

Step 3: add drupal/core-recommended:9.0.0

If you nog add this line and update, we are getting there! First, the first version of drupal 9:

"drupal/core-recommended": "9.0.0",
composer update

This resulted for me in the following:

Problem 1
    - drupal/core 8.0.0-beta6 requires doctrine/common dev-master#a45d110f71c323e29f41eb0696fa230e3fa1b1b5 -> found doctrine/common[2.1.3, ..., 2.13.x-dev, 3.0.0, ..., 3.2.x-dev] but it does not match the constraint.
    - drupal/swiftmailer[1.0.0-beta1, ..., 1.0.0-beta2] require drupal/core ~8.0 -> satisfiable by drupal/core[8.0.0-beta6, ..., 8.9.x-dev].
    - You can only install one version of a package, so only one of these can be installed: drupal/core[8.0.0-beta6, ..., 8.9.x-dev, 9.0.0-alpha1, 

As it appears, there is an incompatibility with drupal/swiftmailer. 

Step 4: remove the modules with issues from composer.json

Above we saw that there was an issue with drupal/swiftmailer. I remove this line from composer:

"drupal/swiftmailer": "^1.0@beta",
composer update

After this, I ran into this with drupal/recaptcha. Repeat the step of removing these lines in composer.json.

There is drupal 9!

Finally, we see our drupal 9 arriving. 

  - Updating twig/twig (v1.42.5 => v2.12.5): Loading from cache
  - Updating symfony/yaml (v3.4.41 => v4.4.9): Loading from cache
  - Downgrading symfony/polyfill-php80 (v1.20.0 => v1.17.0): Loading from cache
  - Updating symfony/var-dumper (v4.4.17 => v5.1.0): Loading from cache
  - Installing symfony/translation-contracts (v2.1.2): Loading from cache
  - Updating symfony/validator (v3.4.41 => v4.4.9): Loading from cache
  - Updating symfony/translation (v3.4.41 => v4.4.9): Loading from cache
  - Installing symfony/service-contracts (v2.1.2): Loading from cache
.........

Step 5: require the modules with issues

In step 4 we removed some modules that caused issues for my D9 upgrade. Now D9 is finally there, we re-add them:

composer require drupal/swiftmailer drupal/recaptcha 

Using version ^2.0 for drupal/swiftmailer
Using version ^3.0 for drupal/recaptcha

For linkit, I had to require a specific version in order for it to work:

composer require drupal/linkit:6.0.0-beta2

Step 6: upgrade to latest drupal 9 version

We are now on Drupal 9.0.0. Perform your database updates first:

drush updb -y

After this, change your drupal/core-recommended line in composer.json to:

"drupal/core-recommended": "^9.0.0",

And update again (do not forget to perform drush updb again after this):

composer update

[sidenote here: in production, it is not always possible to upgrade to 9.0.0 first. It also works to directly push your working drupal 9 composer.json to production and upgrade from 8.9.x to 9.2 and then run database updates]

Solving possible other issues

The why command

There is a 'why'-command to get more info on where the dependencies come from.

composer why symfony/var-dumper

drupal/devel  2.1.0    requires  symfony/var-dumper (~2.7|^3|^4)
drush/drush   9.7.2    requires  symfony/var-dumper (^3.4 || ^4.0)
psy/psysh     v0.10.5  requires  symfony/var-dumper (~5.0|~4.0|~3.0|~2.7)


In this case it was clear that devel and drush are the ones that need to get updated. We add them in our update command and run again:

composer update drupal/core-recommended drush/drush drupal/devel --with-dependencies

composer 2 

Lately I've been stumbling on the following composer 2 issue:

Problem 1
    - Root composer.json requires composer/installers 1.7 -> satisfiable by composer/installers[v1.7.0].
    - composer/installers v1.7.0 requires composer-plugin-api ^1.0 -> found composer-plugin-api[2.0.0] but it does not match the constraint.

Solution: add this to your composer.json:

"composer/installers": "^1.7",

and run

composer update

Porting your custom modules

We've updated our installation, but this does not mean our custom code is compatible. Luckily there is a simple way for checking. The drupal-check package will inspect your code:

composer require mglaman/drupal-check --dev

 Now run

 drupal-check -a web/modules/custom

The result was the following:

 ------ ----------------------------------------------------------------------
  Line   alter_entity_autocomplete/src/EntityAutocompleteMatcher.php
 ------ ----------------------------------------------------------------------
  28     Cannot call method getReferenceableEntities() on object|false.
  35     Call to an undefined static method Drupal::entityManager().
  36     Call to an undefined static method Drupal::entityManager().
 ------ ----------------------------------------------------------------------

Now I need to check the lines of this custom module and fix them for Drupal 9. Do the same for your custom theme:

 drupal-check -a web/themes/custom

In your custom modules, for a starter, change the version constraint from:

core: 8.x

to:

core_version_requirement: ^8 || ^9

Conclusion

It is harder than expected, and you should make sure all your contributed and custom modules are ready.

When you get a better understanding of composer, you'll realize what's going on. 

For me also, this is a warning that you should use as least contributed modules as possible and stick to core!

Still not working? Look at this screencast, it can help you with overcoming your last issues.

Even more possible troubleshooting

Got the error below? Remove type3/... from your composer.json. Will come along with drupal/core-recommended (if necessary)

 Can only install one of: typo3/phar-stream-wrapper[xxxx].

Enable verbose error reporting to debug "unexpected error" white screens: https://stefvanlooveren.me/blog/how-debug-and-enable-php-error-reporting-drupal-8

Other guiding blog posts

https://www.computerminds.co.uk/articles/apply-drupal-9-compatibility-patches-composer


Saved you some valuable time?

Donate and get more free time-saving code on this website in the future.